[
  {
    "path": ".git-hooks/pre-commit",
    "content": "#!/bin/bash\n\nexport PATH=\"$PATH:/opt/homebrew/bin\"\n\nregex=\"\\(Mlem/.*\\).swift$\"\nformatter=$(which swiftformat)\n\ncheck_for_swiftformat() {\n  if [ ! -x \"$formatter\" ]\n  then\n    1>&2 echo \"Unable to find swiftformat - no formatting will take place\"\n    exit 0\n  fi\n}\n\nformat_staged_files() {\n  git diff --diff-filter=d --staged --name-only | grep -e '\\(.*\\).swift$' | while read line; do\n\n    # format the stages changes in a file\n\n    temporary_file=\"${line}.tmp.swift\"\n    git show \":$line\" > \"$temporary_file\"\n\n    $formatter \"$temporary_file\"\n    $formatter \"$line\"\n\n    blob=`git hash-object -w \"$temporary_file\"`\n\n    git update-index --add --cacheinfo 100644 $blob \"$line\"\n\n    rm \"$temporary_file\"\n  done\n}\n\nmain() {\n  check_for_swiftformat\n  format_staged_files\n}\n\nmain\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: Bug Report\ndescription: Report unexpected behavior.\ntype: \"Bug\"\n\nbody:\n- type: checkboxes\n  attributes:\n    label: Requirements\n    description: Please check whether an issue already exists for the bug you encountered.\n    options:\n    - label: There are no existing issues for this bug report.\n      required: true\n- type: textarea\n  attributes:\n    label: \"Description\"\n    description: \"Please describe the issue. If possible, give step-by-step instructions to reproduce the bug.\"\n- type: input\n  attributes:\n    label: \"Mlem Version\"\n    description: \"You can find this under `Settings` -> `About Mlem`. Example: `Mlem 2.0 (184)`.\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/improvement-proposal.yml",
    "content": "name: Improvement Proposal\ndescription: Propose an improvement for Mlem.\ntype: \"Feature\"\n\nbody:\n- type: checkboxes\n  attributes:\n    label: Requirements\n    description: Before you create this issue, please check the following.\n    options:\n    - label: There are no existing issues for this feature.\n      required: true\n    - label: This is a request for a **single** feature (create multiple issues for multiple feature requests).\n      required: true\n- type: textarea\n  attributes:\n    label: \"Description\"\n    description: \"Please describe how you would like Mlem to be improved.\"\n"
  },
  {
    "path": ".github/actions/ci_xcodebuild/action.yml",
    "content": "name: xcodebuild\n\ninputs:\n  xcode_version: \n    required: true\n  xcodebuild_destination:\n    required: true\n  xcodebuild_action:\n    required: true\n\nruns:\n  using: \"composite\"\n  steps:\n  - uses: maxim-lobanov/setup-xcode@v1\n    name: Set Xcode Version\n    with:\n      xcode-version: \"${{ inputs.xcode_version }}\"\n\n  - name: \"Test SDK versions\"\n    shell: bash\n    run: |\n      xcodebuild -showsdks\n\n  - name: \"Xcode Build\"\n    uses: sersoft-gmbh/xcodebuild-action@v3\n    with:\n      project: Mlem.xcodeproj\n      scheme: Mlem\n      configuration: Release\n      destination: \"${{ inputs.xcodebuild_destination }}\"\n      action: \"${{ inputs.xcodebuild_action }}\"\n      result-bundle-path: build_results.xcresult\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--\nThank you for opening a pull request!\n\nWe assume you have read CONTRIBUTING.md. If you have not, do not be surprised if your PR is rejected for reasons listed therein.\n\nPlease note that if your PR does not address an issue that was assigned to you, you are still welcome to open it; however, it will not receive merge priority and may be rejected for scope reasons.\n-->\n\n## Issues\n\n- closes #issue\n- progress towards #issue\n\n## Description\n\n<!-- Describe what this PR does at a high level (e.g., \"adds a toggle to do <thing>\") -->\n\n## Implementation Notes\n\n<!-- Any information about the code that would be helpful for reviewing -->\n"
  },
  {
    "path": ".github/workflows/ci_build_26.yml",
    "content": "name: CI - Build - Xcode 26\n\non:\n    push:\n        branches:\n        - master\n        - dev\n\n    pull_request:\n        branches:\n        - master\n        - dev\n\njobs:\n  Build:\n    permissions: write-all\n    runs-on: macos-26\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v3\n      with:\n        submodules: 'true'\n\n    - name: Build\n      uses: \"./.github/actions/ci_xcodebuild\"\n      with:\n        xcode_version: \"26.4\"\n        xcodebuild_destination: \"platform=iOS Simulator,name=iPhone 17,OS=26.4.1\"\n        xcodebuild_action: \"build\"\n"
  },
  {
    "path": ".github/workflows/ci_lint.yml",
    "content": "name: CI - Lint\n\non:\n  # Run in master as CI\n  push:\n    branches:\n    - master\n      \n  pull_request:\n    branches:\n    - master\n    - dev\n\njobs:\n  SwiftLint:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v1\n    - uses: dorny/paths-filter@v4\n      id: changes\n      with:\n        filters: |\n          src:\n            - '.github/workflows/swiftlint.yml'\n            - '.swiftlint.yml'\n            - '**/*.swift'\n            - 'Mlem.xcodeproj/project.pbxproj'\n    \n    - name: GitHub Action for SwiftLint\n      uses: norio-nomura/action-swiftlint@3.2.1\n      with:\n        args: --strict\n"
  },
  {
    "path": ".gitignore",
    "content": "# Created by https://www.toptal.com/developers/gitignore/api/xcode,swift,macos\n# Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift,macos\n*.orig\n\n### macOS ###\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# https://stackoverflow.com/a/65429032/17629371\nIcon?\n![iI]con[_a-zA-Z0-9]\n\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### macOS Patch ###\n# iCloud generated files\n*.icloud\n\n### Swift ###\n# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n## User settings\nxcuserdata/\n\n## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)\n*.xcscmblueprint\n*.xccheckout\n\n## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)\nbuild/\nDerivedData/\n*.moved-aside\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspectivev3\n\n## Obj-C/Swift specific\n*.hmap\n\n## App packaging\n*.ipa\n*.dSYM.zip\n*.dSYM\n\n## Playgrounds\ntimeline.xctimeline\nplayground.xcworkspace\n\n# Swift Package Manager\n# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.\n# Packages/\n# Package.pins\n# Package.resolved\n# *.xcodeproj\n# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata\n# hence it is not needed unless you have added a package configuration file to your project\n# .swiftpm\n\n.build/\n\n# CocoaPods\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n# Pods/\n# Add this line if you want to avoid checking in source code from the Xcode workspace\n# *.xcworkspace\n\n# Carthage\n# Add this line if you want to avoid checking in source code from Carthage dependencies.\n# Carthage/Checkouts\n\nCarthage/Build/\n\n# fastlane\n# It is recommended to not store the screenshots in the git repo.\n# Instead, use fastlane to re-generate the screenshots whenever they are needed.\n# For more information about the recommended setup visit:\n# https://docs.fastlane.tools/best-practices/source-control/#source-control\n\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots/**/*.png\nfastlane/test_output\n\n# Code Injection\n# After new code Injection tools there's a generated folder /iOSInjectionProject\n# https://github.com/johnno1962/injectionforxcode\n\niOSInjectionProject/\n\n### Xcode ###\n\n## Xcode 8 and earlier\n\n### Xcode Patch ###\n*.xcodeproj/*\n!*.xcodeproj/project.pbxproj\n!*.xcodeproj/xcshareddata/\n!*.xcodeproj/project.xcworkspace/\n!*.xcworkspace/contents.xcworkspacedata\n/*.gcno\n**/xcshareddata/WorkspaceSettings.xcsettings\n\n# End of https://www.toptal.com/developers/gitignore/api/xcode,swift,macos\n\n# Brew\nBrewfile.lock.json\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/MlemApiTypes\"]\n\tpath = Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/MlemApiTypes\n\turl = https://github.com/mlemgroup/MlemApiTypes.git\n\tbranch = master\n"
  },
  {
    "path": ".periphery.yml",
    "content": "project: Mlem.xcodeproj\nschemes:\n- MarkdownUI\n- Mlem\n- Semaphore\ntargets:\n- Mlem\n"
  },
  {
    "path": ".swift-version",
    "content": "5.8\n"
  },
  {
    "path": ".swiftformat",
    "content": "--wraparguments before-first\n--commas inline\n--disable wrapMultilineStatementBraces\n--trimwhitespace nonblank-lines\n--stripunusedargs closure-only\n--header ignore\n--disable self\n--disable headerFileName"
  },
  {
    "path": ".swiftlint.yml",
    "content": "disabled_rules:\n  - trailing_whitespace # disabling this check until swiftformat is in place, which will catch the majority.\n  - todo\nline_length:\n  warning: 140\n  error: 160\n  ignores_comments: true\n  ignores_urls: true\nidentifier_name:\n  excluded: # excluded via string array\n    - id\n    - op\n    - w\n    - h\n    - x\n    - y\n  allowed_symbols: [\"_\"] # these are allowed in type names as we use them in API body arguments\n\nexcluded:\n  - \"**/.build\"\n  - \"**/build\"\n  - Mlem/Packages/MlemMiddleware\n"
  },
  {
    "path": "Additional Documents/EULA.md",
    "content": "# End User License Agreement\n\nWelcome to Mlem! Before you proceed, please carefully read the following Terms of Service (\"Terms\") governing your use of our app. By accessing or using Mlem, you acknowledge that you have read, understood, and agreed to be bound by these Terms. If you do not agree with any part of these Terms, please refrain from using Mlem.\n\n## 1. User Responsibilities\n\n1.1 Reporting Content: Questionable content or content in violation of the rules and guidelines of the Lemmy instance or community on which it is hosted can be reported to the Lemmy community moderators using Mlem's built-in report function or on the instance website. We are not responsible for moderating or enforcing Lemmy instance or community rules.\n\n1.2 Blocking Users and Instances: Mlem provides you with the ability to block individual users, communities, or entire instances. We encourage you to use these blocking features to create a safe and enjoyable Mlem experience.\n\n1.3 Following Instance Rules: You are required to follow the rules of the instance(s) that you access using Mlem. Instance terms of use can be found on the instance website. Failure to comply with the rules of an instance may result in suspension or termination of your account with that instance as dictated by the instance rules.\n\n1.4 No Misuse: The misuse of Mlem will result in termination of all services--to the furthest of our ability--with us. We reserve the right to terminate--to the furthest of our ability--any services that we provide.\n\n1.5 Adult Content: Some Lemmy instances that Mlem accesses may host adult content. Lemmy blocks this content by default; if you wish to view it, in-app or otherwise, you can enable the option on the website of the instance where your account is registered. We take reasonable measures to prevent the display of explicit or adult content within Mlem, but we cannot guarantee that all instances or communities will follow the appropriate procedures to label adult content as such. It is your responsibility to exercise caution while accessing external content.\n\n1.6 No Abusive, Unlawful, or Offensive Content: You may not use Mlem to produce or distribute any abusive, unlawful, or offensive content. This includes, but is not limited to: content that is unlawful, libelous, defamatory, or tortious; harmful, threatening, abusive, invasive, or harassing; or hateful or racially, ethnically, or otherwise discriminatory. The content you produce will not harm minors in any way. You will not impersonate any person or entity. You will not upload or post any content that you do not have the right to make available under any US or foreign laws. You will not produce content that interferes with or disrupts the app, the Lemmy instances that you use, other Lemmy instances, or any other service or person. You will not transmit misinformation in any capacity to any individual.\n\n## 2. Limitation of Liability\n\n2.1 No Liability: To the fullest extent permitted by applicable law, we disclaim any liability for any direct, indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, arising from your use of Mlem or any interactions within the Lemmy instances that you access thereby. This includes, but is not limited to, any damages resulting from the content, actions, or conduct of other Mlem or Lemmy users.\n\n## 3. General Provisions\n\n3.1 Modifications: We reserve the right to modify, suspend, or terminate Mlem or these Terms, at our sole discretion, at any time and without prior notice. Your continued use of Mlem after any modifications to these Terms shall constitute your acceptance of the modified Terms.\n\n3.2 Governing Law: These Terms shall be governed by and construed in accordance with the laws of the United States, without regard to its conflict of laws principles.\n\n3.3 Entire Agreement: These Terms constitute the entire agreement between you and us regarding your use of Mlem and supersede any prior or contemporaneous agreements, communications, or proposals, whether oral or written, between you and us.\n\nBy using Mlem, you affirm that you have read, understood, and agreed to these revised Terms of Service. If you have any questions or concerns, please contact us at mlemappofficial@gmail.com. Thank you for using Mlem!\n"
  },
  {
    "path": "Additional Documents/Privacy.md",
    "content": "# Privacy Policy for Mlem Mobile Application\n\nEffective Date: July 15th, 2023\n\nThank you for using Mlem! This Privacy Policy outlines how your personal information is collected, used, and protected when you use the Mlem mobile application (\"App\"). Please read this Privacy Policy carefully to understand our practices regarding your personal information. By using the Mlem mobile application, you acknowledge that you have read and understood this Privacy Policy and agree to the collection, use, and disclosure of your information as described herein.\n\n## Information We Collect\nMlem does not collect or store any user data. We do not collect any personally identifiable information or track your activities within the App.\n\n## Servers and Data Control\nMlem connects to servers that we do not host or have control over. While we strive to ensure the security and privacy of your data within our App, we cannot guarantee the security or privacy practices of the external servers. Any data stored or processed on these servers is subject to the respective privacy policies and terms of service of those servers.\n\n## Third-Party Services\nMlem does not integrate any third-party services, advertising networks, or analytics tools that collect personal information or track your activities within the App. We prioritize user privacy and do not engage in any data sharing or tracking practices.\n\n## Children's Privacy\nMlem is not intended for use by individuals under the age of 13. We do not knowingly collect personal information from children under the age of 13. If we become aware that we have inadvertently collected personal information from a child under the age of 13, we will take steps to delete the information as soon as possible. If you believe that we may have collected information from a child under the age of 13, please contact us using the information provided in the \"Contact Us\" section below.\n\n## Changes to this Privacy Policy\nWe reserve the right to modify or update this Privacy Policy at any time. Any changes will be effective immediately upon posting the revised Privacy Policy.\n\n## Contact Us\nIf you have any questions, concerns, or requests regarding this Privacy Policy or the privacy practices of Mlem, please contact us at mlemappofficial@gmail.com.\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "*   @mlemgroup/mlem-dev-team\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Hello!\n\nIf you're reading this, you probably want to contribute to Mlem. Welcome! We're happy to have you on board. You may wish to join our [Matrix room](https://matrix.to/#/#mlemappspace:matrix.org) if you haven't already.\n\n## Getting Started\n\n### Cloning and Building\n\nMlem is built using the latest stable version of Xcode. Install it from the App Store or the Apple Developer downloads page, along with the command line tools.\n\nMlem employs submodules to integrate generated code into the main project. To clone the project, execute the following:\n\n`git clone git@github.com:mlemgroup/mlem.git --recurse-submodules`\n\nIf you encounter missing `Api...` types when building, this can usually be resolved by updating the submodules:\n\n`git submodule update --recursive`\n\n### Additional Tools\n\nThis project makes use of the following tools:\n\n- Xcode 15\n- [SwiftLint](https://github.com/realm/SwiftLint#swiftlint). This runs as part of the Xcode build phases.\n- [Swiftformat](https://github.com/nicklockwood/SwiftFormat#what-is-this). This runs as a pre-commit hook.\n\nIn order to benefit please ensure you have [Homebrew](https://brew.sh) installed on your system and then run the following commands inside the project directory:\n\n```\ncd /path/to/this/repo\nbrew update\nbrew bundle\ngit config --local --add core.hooksPath .git-hooks\n```\n\nWith these steps completed each time you build your code will be linted, and each time you commit your code will be formatted.\n\n### Claiming Issues\n\n1. Go to our [project board](https://github.com/orgs/mlemgroup/projects/1/views/1).\n2. Find an unassigned issue under the \"Todo\" section that you'd like to work on.\n3. Comment that you would like to work on the issue. If the issue doesn't conflict with any in-flight work, a maintainer will assign it to you.\n4. Fork the repository (see Cloning and Building) and develop the changes on your fork. It is important that you create your development branch using the upstream `dev` branch as the source, not the `master` branch.\n5. Open a Pull Request for your changes.\n\n## Merge Protocol\n\nWhen your code is approved, it can be merged into the `dev` branch by a member of the development team. If you need to tinker with your changes post-approval, please make a comment that you are doing so. PRs that sit approved for more than 12 hours with no input from the dev may be merged if they are blocking other work.\n\n## Coding Conventions\n\n### General Principles\n\n- Files should be named according to the following patterns:\n  - All files: `TitleCase`. If the file contains extensions, it should be named `BaseEntity+Extensions`.\n  - `View` files: file name must end in `View` (e.g., `FeedsView`)\n- If you can reuse code, do. Prefer abstracting common components to a generic struct and common logic to a generic function.\n\n### Views\n\n- Only one `View` struct should be defined per file\n- Within reason, any complex of views that renders a single component of a larger view should be placed in a descriptively named function, computed property or `@ViewBuilder` variable beneath the body of the View. This keeps pyramids from piling up and makes our accessibility experts' work easier.\n- All `View` structs should be organized according to the following template:\n\n```\nstruct SomeView: View {\n  @AppStorage values\n  @Setting values\n  @Environment entities\n  @Binding variables\n  @State variables\n  @Namespace variables\n  Normal variables\n  Computed properties\n\n  // if necessary\n  init() { ... }\n\n  var body: some View { ... }\n\n  // if necessary\n  var content: some View { ... }\n\n  Helper views\n}\n```\n\n- If the view has modifiers that are attached to the entire body, place the view definition in `content` and attach these modifiers to it in `body` (see `ContentView.swift` for an example).\n- Prefer `var helper: some View` to `func helper() -> some View` unless the helper view takes in parameters.\n- Helper views should always appear lower in the file than the view they help.\n\n### Global Objects\n\nThere are several objects (e.g., `AppState`) that need to be available anywhere in the app. Normally this is handled with `@Environment`, but this is not available outside of the context of a `View`. To address this, globals that need to be available outside of a `View` define a `static var main: GlobalObject = .init()`, allowing them to be referenced as `GlobalObject.main`. This definition should be placed immediately above the initializer.\n\nThis pattern should be used only where necessary, and should not be blindly applied to any global object. Likewise, if possible, these objects should be referenced via `@Environment(GlobalObject.self) var globalObject`; the static singleton should be considered a last resort.\n\n### Colors\n\nColors are managed using our custom `Theming` package, which enables color themes. The following conventions apply:\n\n- Avoid referencing `Color` directly; always use a `Themed` color. These can be referenced the same way normal colors are referenced (e.g., `.fill(.themedSecondary)`)\n- Prefer semantic over literal colors (e.g., `.themedUpvote` over `.blue`).\n\nThe `Theming` package requires the environmental `Palette` object. In certain rare cases, this is not implicitly accessible; if absolutely necessary, a themed color can be generated by explicitly passing in a palette: \n\n`ThemedColor.<color>.resolve(with: palette)`\n\nAvoid using this invocation unless absolutely necessary.\n\n### Main Actor\n\nTo run code on the main actor, use either:\n\n- `@MainActor` annotated method\n- `Task { @MainActor in ... }`\n\nIf you need to execute code after a delay, use `DispatchQueue.main.asyncAfter`.\n\n### Hashable\n\nExplicit `hash` functions for `enum`s should, in the absence of associated values, use a descriptive string to identify each case rather than an integer.\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <https://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    <program>  Copyright (C) <year>  <name of author>\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<https://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<https://www.gnu.org/licenses/why-not-lgpl.html>.\n\n----\n\nThe Mlem iOS developers are aware that the terms of service that apply to apps distributed via Apple's App Store services may conflict with rights granted under\nthe Mlem iOS license, the \"GNU GPLv3\". We have committed not to pursue any license violation that results solely from the conflict between\nthe \"GNU GPLv3\" or any later version and the Apple App Store terms of service.\n"
  },
  {
    "path": "Mlem/App/Actions/ActionSeed+Extensions.swift",
    "content": "//\n//  ActionSeed+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-02.\n//\n\nimport Actions\n\nextension ActionSeed {\n    private static let moderatorActions: Set<ActionSeed> = [\n        .pin,\n        .lock,\n        .markNsfw,\n        .viewVotes,\n        .remove,\n        .banCreator,\n        .purge,\n        .purgeCreator,\n        .resolveReport\n    ]\n\n    var isModeratorAction: Bool {\n        Self.moderatorActions.contains(self)\n    }\n\n    var isBasicAction: Bool {\n        !Self.moderatorActions.contains(self)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ActionSeedSwipeConfiguration.swift",
    "content": "//\n//  ActionSeedSwipeConfiguration.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-04.\n//\n\nimport Actions\nimport Foundation\n\nstruct ActionSeedSwipeConfiguration: Encodable, Equatable {\n    var leading: [ActionSeed]   \n    var trailing: [ActionSeed]\n\n    enum CodingKeys: CodingKey {\n        case leading, trailing\n    }\n\n    func filter(allowed seeds: [ActionSeed]) -> ActionSeedSwipeConfiguration {\n        let keys = Set(seeds.lazy.map(\\.key))\n        return .init(\n            leading: leading.filter { keys.contains($0.key) },\n            trailing: trailing.filter { keys.contains($0.key) }\n        )\n    }\n}\n\nextension ActionSeedSwipeConfiguration {\n    init(from container: KeyedDecodingContainer<CodingKeys>, availableActions: [ActionSeed]) throws {\n        let leading = try container.decode([String].self, forKey: .leading) \n        self.leading = leading.compactMap { key in availableActions.first(where: {$0.key == key}) }\n        let trailing = try container.decode([String].self, forKey: .trailing) \n        self.trailing = trailing.compactMap { key in availableActions.first(where: {$0.key == key}) }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ActionSheet/ActionSheet.swift",
    "content": "//\n//  ActionSheet.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-11-12.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ActionSheetSection {\n    let actions: [any Actions.Action]\n}\n\nstruct ActionSheet: View {\n    @Environment(\\.dismiss) var dismiss\n    @Environment(NavigationLayer.self) var navigation\n\n    let sections: [ActionSheetSection]\n    let environment: EnvironmentValues\n    let configuration: ContextMenuSettingsPage?\n\n    @State var popupAnchorModel: PopupAnchorModel = .init()\n\n    var body: some View {\n        ScrollView {\n            VStack(alignment: .leading, spacing: 0) {\n                content\n                    .padding(16)\n                if let configuration {\n                    Button(\"Customize\", icon: .general.edit) {\n                        navigation.replace(.settings(.contextMenu(configuration)))\n                    }\n                    .font(.footnote)\n                    .padding(.horizontal, 32)\n                    .padding(.top, -5)\n                }\n            }\n        }\n        .presentationBackground(.themedGroupedBackground)\n        .presentationDragIndicator(.hidden)\n        .presentationBackgroundInteraction(.enabled)\n    }\n\n    var content: some View {\n        ForEach(Array(sections.enumerated()), id: \\.offset) { _, section in\n            let frames = frames(for: section.actions)\n            if !frames.isEmpty {\n                VStack(spacing: 0) {\n                    ForEach(Array(frames.enumerated()), id: \\.offset) { index, frame in\n                        actionRow(frame, showDivider: ![frames.startIndex, frames.endIndex].contains(index))\n                            .compositingGroup()\n                    }\n                }\n                .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: 25))\n            }\n        }\n        .labelStyle(ActionSheetLabelStyle())\n        .buttonStyle(ActionSheetButtonStyle())\n        .onChange(of: popupAnchorModel.outcome) {\n            if popupAnchorModel.outcome == .confirmed, !navigation.rootChangePending { dismiss() }\n        }\n    }\n\n    private func frames(for actions: [any Actions.Action]) -> [ActionFrame] {\n        actions.compactMap {\n            let label = $0.createLabel(environment: environment)\n            if label.visibility == .hidden { return nil }\n            return .init(action: $0, label: label)\n        }\n    }\n\n    @ViewBuilder\n    private func actionRow(_ frame: ActionFrame, showDivider: Bool) -> some View {\n        if showDivider {\n            Divider()\n                .padding(.horizontal, 15)\n        }\n        ActionSheetButton(action: frame.action, label: frame.label)\n            .popupAnchor(model: popupAnchorModel)\n            .environment(navigation)\n            .environment(\\.self, environment)\n    }\n}\n\nprivate struct ActionSheetButton: View {\n    @Environment(\\.dismiss) var dismiss\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(PopupAnchorModel.self) var popupAnchorModel\n    @Environment(\\.self) var environment\n\n    let action: any Actions.Action\n\n    // Lable passed separately for performance reasons\n    let label: ActionLabel\n\n    var body: some View {\n        Button(label) {\n            action.execute(environment: environment)\n            if !navigation.rootChangePending, popupAnchorModel.data == nil {\n                dismiss()\n            }\n        }\n        .disabled(label.visibility == .disabled)\n    }\n}\n\nprivate struct ActionFrame {\n    let action: any Actions.Action\n    let label: ActionLabel\n}\n\nprivate struct ActionSheetButtonStyle: ButtonStyle {\n    func makeBody(configuration: Configuration) -> some View {\n        configuration.label\n            .foregroundStyle(configuration.role == .destructive ? .themedWarning : .themedPrimary)\n    }\n}\n\nprivate struct ActionSheetLabelStyle: LabelStyle {\n    @ScaledMetric(relativeTo: .body) var rowHeight = 40\n\n    func makeBody(configuration: Configuration) -> some View {\n        HStack {\n            configuration.title\n            Spacer()\n            configuration.icon\n                .font(.title2)\n        }\n        .padding(.horizontal, 25)\n        .frame(height: rowHeight)\n        .padding(.vertical, 8)\n        .contentShape(.rect)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ActionSheet/CustomizableContextMenu.swift",
    "content": "//\n//  CustomizableActionMenu.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-02.\n//\n\nimport Actions\nimport SwiftUI\n\nstruct CustomizableActionMenu<Configuration: ContextMenuConfiguration>: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.self) var environment\n\n    let configurationKeyPathGenerator: (EnvironmentValues) -> ReferenceWritableKeyPath<SettingsValues, Configuration>\n    let createAction: (ActionSeed, EnvironmentValues) -> (any Actions.Action)?\n    let customizable: Bool\n\n    fileprivate init(\n        customizable: Bool = true,\n        configuration keyPathGenerator: @escaping (EnvironmentValues) -> ReferenceWritableKeyPath<SettingsValues, Configuration>,\n        createAction: @escaping (ActionSeed, EnvironmentValues) -> (any Actions.Action)?,\n    ) {\n        self.configurationKeyPathGenerator = keyPathGenerator\n        self.customizable = customizable\n        self.createAction = createAction\n    }\n\n    var configuration: Configuration {\n        Settings.get(configurationKeyPathGenerator(environment))\n    }\n\n    var body: some View {\n        ActionButtons { _ in\n            self.createActions(seeds: configuration.contextMenu)\n        }\n        .environment(\\.isContextMenu, true)\n        if customizable {\n            Section {\n                Button(\"More...\", icon: .general.menu) {\n                    navigation.openSheet(.actionSheet(\n                        sheetSections,\n                        environment: environment,\n                        configuration: configurationKeyPathGenerator(environment)\n                    ))\n                }\n                .symbolVariant(.circle)\n            }\n        }\n    }\n\n    var sheetSections: [ActionSheetSection] {\n        Configuration.availableActions.sections.map { seeds in\n            .init(actions: self.createActions(seeds: seeds))\n        }\n    }\n\n    func createActions(seeds: [ActionSeed]) -> [any Actions.Action] {\n        seeds.compactMap { self.createAction($0, environment) }\n    }\n}\n\nextension CustomizableActionMenu {\n    init(\n        entity: Any,\n        configuration keyPath: ReferenceWritableKeyPath<SettingsValues, Configuration>,\n        customizable: Bool = true\n    ) {\n        self.init(configuration: keyPath, customizable: customizable) { seed, _ in\n            seed.createAction(entity)\n        }\n    }\n\n    init(\n        configuration keyPath: ReferenceWritableKeyPath<SettingsValues, Configuration>,\n        customizable: Bool = true,\n        createAction: @escaping (ActionSeed, EnvironmentValues) -> (any Actions.Action)?,\n    ) {\n        self.configurationKeyPathGenerator = { _ in keyPath }\n        self.customizable = customizable\n        self.createAction = createAction\n    }\n\n    init(\n        entity: Any,\n        configuration: ReferenceWritableKeyPath<SettingsValues, Configuration>,\n        modMailConfiguration: ReferenceWritableKeyPath<SettingsValues, Configuration>,\n        customizable: Bool = true,\n        _ filter: @escaping (ActionSeed) -> Bool = { _ in true }\n    ) {\n        self.init(\n        customizable: customizable,\n        configuration: { environment in\n            if environment.reportContext != nil && Settings.get(\\.interactionBar_alternateReportLayout) {\n                configuration\n            } else {\n                modMailConfiguration\n            }\n        },\n        createAction: { seed, environment in\n            if !filter(seed) { return nil }\n            if let report = environment.reportContext {\n                if let action = seed.createAction(report) {\n                    return action\n                }\n            }\n            return seed.createAction(entity)\n        })\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/AppointAdminAction.swift",
    "content": "//\n//  AppointAdminAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct AppointAdminAction: Actions.Action {\n    let entity: Person\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let appointAdmin = ActionSeed(\"appointAdmin\", label: AppointAdminAction.appointLabel) { entity in\n        switch entity {\n        case let entity as Person:\n            AppointAdminAction(entity: entity)\n        default:\n            nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension AppointAdminAction {\n    static let appointLabel: ActionLabel = .init(\n        \"Appoint Administrator\",\n        icon: .lemmy.addAdministrator,\n        color: .themedPositive\n    )\n\n    static let demoteLabel: ActionLabel = .init(\n        \"Remove Administrator\",\n        icon: .lemmy.removeAdministrator,\n        color: .themedNegative\n    )\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        guard let isAdmin = entity.isAdmin.value else { return Self.demoteLabel.withVisibility(.hidden) }\n        let label: ActionLabel\n\n        if isAdmin {\n            label = Self.demoteLabel\n        } else {\n            label = Self.appointLabel\n        }\n\n        return label.withVisibility(visibility(environment))\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard let entityIsAdmin = entity.isAdmin.value else { return .hidden }\n        if entity.api.canInteract(appState: environment.appState),\n            entity.api.isAdmin,\n            !entityIsAdmin,\n            entity.api.isHigherAdmin(than: entity),\n            entity.apiIsLocal {\n            return .enabled\n        } else {\n            return .hidden\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension AppointAdminAction {\n    func execute(environment: EnvironmentValues) {\n        guard let message = self.popupMessage(environment: environment) else {\n            assertionFailure()\n            return\n        }\n        environment.popupModel?.showPopup(message: message, [\n            .init(title: \"Yes\", isDestructive: true) {\n                confirm(environment: environment)\n            }\n        ])\n    }\n\n    private func popupMessage(environment: EnvironmentValues) -> LocalizedStringResource? {\n        guard let isAdmin = self.entity.isAdmin.value else { return nil }\n        if isAdmin {\n            return \"Really remove administrator \\(entity.displayName) from \\(self.entity.api.host)?\"\n        } else {\n            return \"Really appoint \\(entity.displayName) as an administrator of \\(self.entity.api.host)?\"\n        }\n    }\n\n    private func confirm(environment: EnvironmentValues) {\n        guard let instance = entity.api.myInstance,\n        let isAdmin = entity.isAdmin.value else {\n            assertionFailure()\n            return\n        }\n\n        instance.addAdmin(personId: self.entity.id, added: !isAdmin)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/AppointModeratorAction.swift",
    "content": "//\n//  AppointModeratorAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct AppointModeratorAction: Actions.Action {\n    let entity: Person\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let appointModerator = ActionSeed(\"appointModerator\", label: AppointModeratorAction.appointLabel) { entity in\n        switch entity {\n        case let entity as Person:\n            AppointModeratorAction(entity: entity)\n        default:\n            nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension AppointModeratorAction {\n    static let appointLabel: ActionLabel = .init(\n        \"Appoint Moderator\",\n        icon: .lemmy.addModerator,\n        color: .themedPositive\n    )\n\n    static let demoteLabel: ActionLabel = .init(\n        \"Remove Moderator\",\n        icon: .lemmy.removeModerator,\n        color: .themedNegative\n    )\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        let label: ActionLabel\n\n        if isModerator(environment: environment) ?? false {\n            label = Self.demoteLabel\n        } else {\n            label = Self.appointLabel\n        }\n\n        return label.withVisibility(visibility(environment))\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        if let communityModerators = environment.communityContext?.moderators.value,\n            let myPerson = entity.api.myPerson,\n            entity.api.canInteract(appState: environment.appState),\n            myPerson.canModerate(entity, communityModerators: communityModerators) {\n            .enabled\n        } else {\n            .hidden\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension AppointModeratorAction {\n    func isModerator(environment: EnvironmentValues) -> Bool? {\n        if let communityModerators = environment.communityContext?.moderators.value {\n            return communityModerators.contains(where: { $0.id == entity.id })\n        } else {\n            return nil\n        }\n    }\n\n    func execute(environment: EnvironmentValues) {\n        guard let message = self.popupMessage(environment: environment) else {\n            assertionFailure()\n            return\n        }\n        environment.popupModel?.showPopup(message: message, [\n            .init(title: \"Yes\", isDestructive: true) {\n                confirm(environment: environment)\n            }\n        ])\n    }\n\n    private func popupMessage(environment: EnvironmentValues) -> LocalizedStringResource? {\n        guard let community = environment.communityContext else { return nil }\n\n        if self.isModerator(environment: environment) ?? false {\n            return \"Really remove moderator \\(entity.displayName) from \\(community.displayName)?\"\n        } else {\n            return \"Really appoint \\(entity.displayName) as a moderator of \\(community.displayName)?\"\n        }\n    }\n\n    private func confirm(environment: EnvironmentValues) {\n        guard let isModerator = self.isModerator(environment: environment) else {\n            assertionFailure()\n            return\n        }\n        Task {\n            do {\n                try await environment.communityContext?.addModerator(self.entity, added: !isModerator)\n            } catch {\n                handleError(error)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/BanAction.swift",
    "content": "//\n//  BanAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nprivate enum BanScope {\n    case community\n    case instance\n}\n\nprivate extension Set<BanScope> {\n    static let communityOnly: Set<BanScope> = [.community]\n    static let instanceOnly: Set<BanScope> = [.instance]\n    static let both: Set<BanScope> = [.community, .instance]\n    static let none: Set<BanScope> = []\n}\n\nprivate struct BanScopePattern {\n    let closure: (Set<BanScope>) -> Bool\n\n    static func ~= (lhs: BanScopePattern, rhs: Set<BanScope>) -> Bool {\n        lhs.closure(rhs)\n    }\n}\n\nprivate extension BanScopePattern {\n    static func anyContaining(_ value: BanScope) -> BanScopePattern {\n        BanScopePattern { $0.contains(value) }\n    }\n\n    static func anyNotContaining(_ value: BanScope) -> BanScopePattern {\n        BanScopePattern { !$0.contains(value) }\n    }\n}\n\nstruct BanAction: SimpleLabelAction {\n    let entity: Person\n\n    var canBanFromInstance: Bool {\n        entity.api.isAdmin && entity.api.supports(.banFromInstance, defaultValue: false)\n    }\n\n    func canBanFromCommunity(community: Community?) -> Bool {\n        let supportedByApi = entity.api.supports(.banFromCommunity, defaultValue: false) && (\n            entity.apiIsLocal || entity.api.supports(.banFromNonLocalCommunity, defaultValue: false)\n        )\n\n        guard supportedByApi else { return false }\n        guard let community else { return entity.api.isAdmin }\n        guard let myPerson = entity.api.myPerson,\n              let myPersonModerates = myPerson.moderates else { return false }\n\n        return myPersonModerates(.community(community)) || entity.api.isAdmin\n    }\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let ban = ActionSeed(\"ban\") { entity in\n        switch entity {\n        case let entity as Person: BanAction(entity: entity)\n        default: nil\n        }\n    }\n\n    static let banCreator = ActionSeed(\"banCreator\") { entity in\n        switch entity {\n        case let entity as Comment:\n            if let creator = entity.creator.value {\n                BanAction(entity: creator)\n            } else {\n                nil\n            }\n        case let entity as Post:\n            if let creator = entity.creator.value {\n                BanAction(entity: creator)\n            } else {\n                nil\n            }\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension BanAction {\n    static let label: ActionLabel = .init(\n        \"Ban\",\n        icon: .lemmy.banFromCommunity,\n        color: .themedNegative,\n        isDestructive: true\n    )\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        let label: ActionLabel\n\n        let appliedBanScopes = getAppliedBanScopes(environment: environment)\n        let actionableBanScopes = getActionableBanScopes(environment: environment)\n\n        switch (bannedFrom: appliedBanScopes, canBanFrom: actionableBanScopes) {\n        case (bannedFrom: .none, canBanFrom: .both),\n             (bannedFrom: .anyNotContaining(.instance), canBanFrom: .instanceOnly):\n            label = .init(\n                \"Ban\",\n                icon: .lemmy.banFromInstance,\n                color: .themedNegative,\n                isDestructive: true\n            )\n\n        case (bannedFrom: .anyContaining(.instance), canBanFrom: .instanceOnly),\n             (bannedFrom: .both, canBanFrom: .both):\n            label = .init(\n                \"Unban\",\n                icon: .lemmy.unbanFromInstance,\n                color: .themedPositive\n            )\n\n        case (bannedFrom: .instanceOnly, canBanFrom: .both),\n             (bannedFrom: .communityOnly, canBanFrom: .both):\n            label = .init(\n                \"Ban...\",\n                icon: .lemmy.banFromInstance,\n                color: .themedNegative,\n                isDestructive: true\n            )\n\n        case (bannedFrom: .anyContaining(.community), canBanFrom: .communityOnly):\n            label = .init(\n                \"Unban\",\n                icon: .lemmy.unbanFromCommunity,\n                color: .themedPositive\n            )\n\n        case (bannedFrom: .anyNotContaining(.community), canBanFrom: .communityOnly):\n            label = Self.label\n\n        default:\n            return Self.label.withVisibility(.hidden)\n        }\n\n        return label.withVisibility(visibility(environment))\n    }\n\n    /// Get the scopes that the target is current banned within.\n    private func getAppliedBanScopes(environment: EnvironmentValues) -> Set<BanScope> {\n        var output: Set<BanScope> = []\n        if isBannedFromCommunity(environment: environment) {\n            output.insert(.community)\n        }\n        if entity.bannedFromInstance {\n            output.insert(.instance)\n        }\n        return output\n    }\n\n    /// Get the set of ban scopes that the authorized user is able to apply to the target.\n    private func getActionableBanScopes(environment: EnvironmentValues) -> Set<BanScope> {\n        var output: Set<BanScope> = []\n        if canBanFromCommunity(community: environment.communityContext) {\n            output.insert(.community)\n        }\n        if entity.api.isAdmin {\n            output.insert(.instance)\n        }\n        return output\n    }\n\n    private func isBannedFromCommunity(environment: EnvironmentValues) -> Bool {\n        guard let communityContext = environment.communityContext else { return false }\n        return entity.isBannedFromCommunity(communityContext) ?? false\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard entity.api.canInteract(appState: environment.appState) else { return .hidden }\n        \n        guard let myPersonId = entity.api.myPerson?.id else { return .hidden }\n        return entity.id == myPersonId ? .hidden : .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension BanAction {\n    // If `nil` is returned, a modal should be shown asking whether the user wants to ban or unban\n    private func shouldBan(environment: EnvironmentValues) -> Bool? {\n        let bannedFromCommunity = isBannedFromCommunity(environment: environment)\n        let bannedFromInstance = entity.bannedFromInstance\n\n        if canBanFromInstance {\n            switch (bannedFromCommunity, bannedFromInstance) {\n            case (false, false):\n                return true\n            case (true, true):\n                return false\n            default:\n                return nil\n            }\n        } else {\n            return !bannedFromCommunity\n        }\n    }\n\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        if let shouldBan = shouldBan(environment: environment) {\n            showBanSheet(environment: environment, shouldBan: shouldBan)\n        } else {\n            showAlert(environment: environment)\n        }\n    }\n\n    @MainActor\n    private func showAlert(environment: EnvironmentValues) {\n        var actions: [PopupAnchorModel.Action] = []\n\n        if entity.bannedFromInstance {\n            actions.append(\n                .init(title: \"Unban from Instance\", isDestructive: false) {\n                    showBanSheet(environment: environment, shouldBan: false)\n                }\n            )\n        } else {\n            actions.append(\n                .init(title: \"Ban from Instance\", isDestructive: true) {\n                    showBanSheet(environment: environment, shouldBan: true)\n                }\n            )\n        }\n\n        if isBannedFromCommunity(environment: environment) {\n            actions.append(\n                .init(title: \"Unban from Community\", isDestructive: false) {\n                    showBanSheet(environment: environment, shouldBan: false)\n                }\n            )\n        } else {\n            actions.append(\n                .init(title: \"Ban from Community\", isDestructive: true) {\n                    showBanSheet(environment: environment, shouldBan: true)\n                }\n            )\n        }\n\n        environment.popupModel?.showPopup(message: \"Choose an action...\", actions)\n    }\n\n    @MainActor\n    private func showBanSheet(environment: EnvironmentValues, shouldBan: Bool) {\n        environment.navigation?.openSheet(.ban(\n            entity,\n            isBannedFromCommunity: isBannedFromCommunity(environment: environment),\n            shouldBan: shouldBan,\n            community: environment.communityContext\n        ))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/BlockAction.swift",
    "content": "//\n//  BlockAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\nimport MlemBackend\n\nstruct BlockAction: Actions.Action {\n    enum Relationship { case direct, indirect }\n\n    enum ContentType {\n        case personOnly, communityOnly, instanceOnly, multi, other\n    }\n\n    let content: [any Blockable]\n    let relationship: Relationship\n\n    var availableContent: [any Blockable] {\n        content.filter { item in\n            switch item {\n            case let entity as Person:\n                guard let myPersonId = entity.api.myPerson?.id else { return true }\n                return entity.id != myPersonId \n            default:\n                return true\n            }\n        }\n    }\n}\n\nprivate extension [Blockable] {\n    var contentType: BlockAction.ContentType {\n        if self.count > 1 {\n            return .multi\n        }\n        guard let first = self.first else {\n            return .other\n        } \n\n        return switch first {\n        case _ as Person: .personOnly\n        case _ as Community: .communityOnly\n        case _ as Instance: .instanceOnly\n        default: .other\n        }\n    }\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let block = ActionSeed(\n        \"block\",\n        label: BlockAction.createLabel(relationship: .direct, mode: .block, contentType: .multi)\n    ) { entity in\n        switch entity {\n        case let entity as any Blockable: BlockAction(content: [entity], relationship: .direct)\n        default: nil\n        }\n    }\n\n    static let blockCreator = ActionSeed(\n        \"blockCreator\",\n        label: BlockAction.createLabel(relationship: .indirect, mode: .block, contentType: .multi)\n    ) { entity in\n        switch entity {\n        case let entity as Comment:\n            if let creator = entity.creator.value {\n                BlockAction(\n                    content: [creator],\n                    relationship: .indirect)\n            } else {\n                nil\n            }\n        case let entity as Post:\n            if let creator = entity.creator.value, let community = entity.community.value {\n                BlockAction(\n                    content: [creator, community],\n                    relationship: .indirect)\n            } else {\n                nil\n            }\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension BlockAction {\n    enum Mode { case block, unblock }\n\n    // swiftlint:disable:next cyclomatic_complexity\n    static func createLabel(relationship: Relationship, mode: Mode, contentType: ContentType) -> ActionLabel {\n        let label: LocalizedStringResource = switch (relationship, mode, contentType) {\n        case (.direct, .block, _): \"Block\"\n        case (.direct, .unblock, _): \"Unblock\"\n        case (.indirect, .block, .personOnly): \"Block User\"\n        case (.indirect, .unblock, .personOnly): \"Unblock User\"\n        case (.indirect, .block, .communityOnly): \"Block Community\"\n        case (.indirect, .unblock, .communityOnly): \"Unblock Community\"\n        case (.indirect, .block, .instanceOnly): \"Block Instance\"\n        case (.indirect, .unblock, .instanceOnly): \"Unblock Instance\"\n        case (.indirect, .block, .multi): \"Block...\"\n        case (.indirect, .unblock, .multi): \"Unblock...\"\n        case (_, _, .other): \"Block...\"\n        }\n\n        return switch mode {\n        case .block: .init(\n            label,\n            icon: .lemmy.block,\n            color: .themedNegative,\n            isDestructive: true\n        )\n        case .unblock: .init(\n            label,\n            icon: .lemmy.unblock,\n            color: .themedPositive\n        )\n        }\n    }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        return Self.createLabel(\n            relationship: self.relationship,\n            mode: content.first!.blocked(environment: environment) ? .unblock : .block,\n            contentType: availableContent.contentType\n        ).withVisibility(visibility(environment))\n    }\n\n    // swiftlint:disable:next cyclomatic_complexity\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        let canInteract = content.allSatisfy {\n            if $0 is any InstanceActionProviding {\n                return true\n            } else if let contentModel = $0 as? ContentModel {\n                return contentModel.api.canInteract(appState: environment.appState) && $0.updateBlocked != nil\n            }\n            return false\n        }\n        guard canInteract else { return .hidden }\n\n        for item in content {\n            switch item {\n            case let person as Person:\n                guard let myPersonId = person.api.myPerson?.id else { return .hidden }\n                guard person.id != myPersonId else { return .hidden }\n            case let instance as any InstanceActionProviding:\n                let api = environment.appState.firstApi\n                guard api.supports(.blockInstances, defaultValue: false) else { return .hidden }\n                guard api.actorId != instance.actorId else { return .hidden }\n            default:\n            break\n            }\n        }\n\n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension BlockAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        if availableContent.count > 1 {\n            executeMulti(environment: environment)\n            return\n        }\n\n        guard let first = availableContent.first else {\n            assertionFailure()\n            return\n        }\n\n        execute(entity: first, environment: environment)\n    }\n\n    @MainActor\n    func executeMulti(environment: EnvironmentValues) {\n        let actions: [PopupAnchorModel.Action] = content.map { item in\n            let callback = {\n                submit(entity: item, environment: environment)\n            }\n            let label = Self.createLabel(\n                relationship: .indirect,\n                mode: item.blocked(environment: environment) ? .unblock : .block,\n                contentType: item is Person ? .personOnly: .communityOnly\n            )\n            return .init(\n                title: label.title,\n                isDestructive: label.isDestructive,\n                callback: callback\n            )\n        }\n        environment.popupModel?.showPopup(message: \"User or community?\", actions)\n    }\n\n    @MainActor\n    func execute(entity: any Blockable, environment: EnvironmentValues) {\n        if entity.blocked(environment: environment) {\n            submit(entity: entity, environment: environment)\n            return\n        }\n\n        let label: String\n\n        switch entity {\n        case _ as Person:\n            label = .init(localized: \"Really block this user?\")\n        case _ as Community:\n            label = .init(localized: \"Really block this community?\")\n        case _ as any InstanceActionProviding:\n            label = .init(localized: \"Really block this instance?\")\n        default:\n            assertionFailure()\n            label = \"Really block?\"\n        }\n\n        environment.popupModel?.showPopup(message: label, [\n            .init(title: \"Yes\", isDestructive: true) {\n                submit(entity: entity, environment: environment)\n            }\n        ])\n    }\n\n    private func submit(entity: any Blockable, environment: EnvironmentValues) {\n        let shouldBlock = !entity.blocked(environment: environment)\n        if let updateBlocked = entity.updateBlocked {\n            updateBlocked(shouldBlock) { didSucceed in\n                let toast = createToast(didBlock: shouldBlock, didSucceed: didSucceed) {\n                    updateBlocked(!shouldBlock, nil)\n                }\n                environment.toastModel?.add(toast)\n            }\n        } else if entity is any InstanceActionProviding, let session = (environment.appState.firstSession as? UserSession) {\n            session.updateInstanceBlock(actorId: entity.actorId, shouldBlock: shouldBlock) { didSucceed in\n                let toast = createToast(didBlock: shouldBlock, didSucceed: didSucceed) {\n                    session.updateInstanceBlock(actorId: entity.actorId, shouldBlock: !shouldBlock)\n                }\n                environment.toastModel?.add(toast)\n            }\n        } else {\n            assertionFailure(\"Failed to block entity\")\n        }\n    }\n\n    private func createToast(\n        didBlock: Bool,\n        didSucceed: Bool,\n        undo: @escaping () -> Void\n    ) -> ToastType {\n        switch (didBlock, didSucceed) {\n        case (true, true): .undoable(\n                \"Blocked\",\n                icon: .lemmy.block,\n                callback: undo,\n                color: .themedNegative\n            )\n        case (true, false): .failure(\"Failed to block!\")\n        case (false, true): .basic(\"Unblocked\", icon: .lemmy.unblock)\n        case (false, false): .failure(\"Failed to unblock!\")\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/CollapseAction.swift",
    "content": "//\n//  CollapseAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-15.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CollapseAction: SimpleLabelAction {\n    let entity: Comment\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let collapse = ActionSeed(\"collapse\") { entity in\n        switch entity {\n        case let entity as Comment: CollapseAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension CollapseAction {\n    static let collapseLabel: ActionLabel = .init(\n        \"Collapse\",\n        icon: .general.collapse,\n        color: .themedColorfulAccent(0)\n    )\n\n    static let expandLabel: ActionLabel = .init(\n        \"Expand\",\n        icon: .general.expand,\n        color: .themedColorfulAccent(0)\n    )\n\n   static var label: ActionLabel { collapseLabel }\n\n   func createLabel(environment: EnvironmentValues) -> ActionLabel {\n       guard let node = environment.commentTreeTracker?.getNode(actorId: entity.actorId) else {\n           return Self.label.withVisibility(.hidden)\n       } \n       if node.collapsed {\n           return Self.expandLabel\n       } else {\n           return Self.collapseLabel\n       }\n    }\n}\n\n// MARK: - Behavior\n\nextension CollapseAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        if let node = environment.commentTreeTracker?.getNode(actorId: entity.actorId) {\n            withAnimation(UIAccessibility.isReduceMotionEnabled ? nil : .default) {\n                node.collapsed.toggle()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/CollapseParentAction.swift",
    "content": "//\n//  CollapseParentAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-15.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CollapseParentAction: SimpleLabelAction {\n    let entity: Comment\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let collapseParent = ActionSeed(\"collapseParent\") { entity in\n        switch entity {\n        case let entity as Comment: CollapseParentAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension CollapseParentAction {\n    static let label: ActionLabel = .init(\n        \"Collapse Parent\",\n        icon: .lemmy.collapseParent,\n        color: .themedColorfulAccent(0)\n    )\n\n   func createLabel(environment: EnvironmentValues) -> ActionLabel {\n       return Self.label.withVisibility(visibility(environment: environment))\n    }\n\n    func visibility(environment: EnvironmentValues) -> ActionVisiblity {\n        if environment.commentTreeTracker?.hasNode(actorId: entity.actorId) ?? false {\n            .enabled\n        } else {\n            .hidden\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension CollapseParentAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        if let node = environment.commentTreeTracker?.getNode(actorId: entity.actorId) {\n            withAnimation(UIAccessibility.isReduceMotionEnabled ? nil : .default) {\n                (node.parent ?? node).collapsed.toggle()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/CollapseToTopAction.swift",
    "content": "//\n//  CollapseToTopAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-15.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CollapseToTopAction: SimpleLabelAction {\n    let entity: Comment\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let collapseToTop = ActionSeed(\"collapseToTop\") { entity in\n        switch entity {\n        case let entity as Comment: CollapseToTopAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension CollapseToTopAction {\n    static let label: ActionLabel = .init(\n        \"Collapse to Top\",\n        icon: .lemmy.collapseToTop,\n        color: .themedColorfulAccent(0)\n    )\n\n   func createLabel(environment: EnvironmentValues) -> ActionLabel {\n       return Self.label.withVisibility(visibility(environment: environment))\n    }\n\n    func visibility(environment: EnvironmentValues) -> ActionVisiblity {\n        if environment.commentTreeTracker?.hasNode(actorId: entity.actorId) ?? false {\n            .enabled\n        } else {\n            .hidden\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension CollapseToTopAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        if let node = environment.commentTreeTracker?.getNode(actorId: entity.actorId) {\n            withAnimation(UIAccessibility.isReduceMotionEnabled ? nil : .default) {\n                node.topParent.collapsed.toggle()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ContextMenu+Comment.swift",
    "content": "//\n//  ContextMenu+Comment.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-17.\n//\n\nimport Actions\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\n\nprivate let seeds: [ActionSeed] = [\n    .upvote,\n    .downvote,\n    .save,\n    .reply,\n    .selectText,\n    .share,\n    .createImage,\n    .report,\n    .blockCreator,\n    .edit,\n    .delete\n]\n\nprivate let moderationSeeds: [ActionSeed] = [\n    .viewVotes,\n    .remove,\n    .banCreator,\n    .purge,\n    .purgeCreator\n]\n\nextension View {\n    func contextMenu(comment: Comment) -> some View {\n        contextMenu {\n            CustomizableActionMenu(\n                entity: comment,\n                configuration: \\.interactionBar_comment,\n                modMailConfiguration: \\.interactionBar_commentReport,\n                customizable: true\n            )\n        }\n        .popupAnchor()\n    }\n\n    @ViewBuilder\n    func quickSwipes(comment: Comment, configuration: CommentBarConfiguration) -> some View {\n        quickSwipes(\n            leading: configuration.swipes.leading.compactMap { $0.createAction(comment) },\n            trailing: configuration.swipes.trailing.compactMap { $0.createAction(comment) }\n        )\n    }\n}\n\nextension EllipsisMenu {\n    init(\n        icon: Icon = .general.menu,\n        size: CGFloat,\n        comment: Comment,\n        type: Set<EllipsisMenuType> = [.basic, .moderator]\n    ) where Content == CustomizableActionMenu<CommentBarConfiguration> {\n        self.icon = icon\n        self.size = size\n\n        self.content = CustomizableActionMenu(\n            entity: comment,\n            configuration: \\.interactionBar_comment,\n            modMailConfiguration: \\.interactionBar_commentReport,\n            customizable: true\n        ) { seed in\n            if seed.isModeratorAction {\n                return type.contains(.moderator)\n            } else {\n                return type.contains(.basic)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ContextMenu+Community.swift",
    "content": "//\n//  ContextMenu+Community.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-02-08.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nextension ActionButtons {\n    init(community: Community) {\n        self.init { _ in\n            CommunityActionConfiguration.availableActions.all.compactMap { $0.createAction(community) }\n        }\n    }\n}\n\nextension View {\n    func contextMenu(community: Community) -> some View {\n        contextMenu {\n            ActionButtons(community: community)\n        }\n    }\n\n    @ViewBuilder\n    func quickSwipes(community: Community, configuration: CommunityActionConfiguration) -> some View {\n        quickSwipes(\n            leading: configuration.swipes.leading.compactMap { $0.createAction(community) },\n            trailing: configuration.swipes.trailing.compactMap { $0.createAction(community) }\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ContextMenu+InboxNotification.swift",
    "content": "//\n//  ContextMenu+InboxNotification.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-11-07.\n//  \n\nimport Actions\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\n\nextension View {\n    func contextMenu(notification: InboxNotification) -> some View {\n        contextMenu {\n            CustomizableActionMenu(configuration: \\.interactionBar_reply) { seed, _ in\n                seed.createAction(notification) ?? seed.createAction(notification.content.wrappedValue)\n            }\n        }\n    }\n\n    func contextMenu(notification: InboxNotification?, message: any Message, report: Report?) -> some View {\n        contextMenu {\n            CustomizableActionMenu(configuration: \\.interactionBar_reply) { seed, _ in\n                if let notification {\n                    if let action = seed.createAction(notification) { return action }\n                }\n                if let report {\n                    if let action = seed.createAction(report) { return action }\n                }\n                return seed.createAction(message)\n            }\n        }\n    }\n\n    @ViewBuilder\n    func quickSwipes(notification: InboxNotification, configuration: ReplyBarConfiguration) -> some View {\n        quickSwipes(\n            leading: configuration.swipes.leading.compactMap { seed in\n                seed.createAction(notification) ?? notification.content.comment.map { seed.createAction($0) } ?? nil\n            },\n            trailing: configuration.swipes.trailing.compactMap { seed in\n                seed.createAction(notification) ?? notification.content.comment.map { seed.createAction($0) } ?? nil\n            }\n        )\n    }\n}\n\nprivate extension InboxNotificationContent {\n    var comment: Comment? {\n        switch self {\n        case let .reply(comment), let .mention(comment): comment\n        default: nil\n        }\n    }\n}\n\nextension EllipsisMenu {\n    init(\n        icon: Icon = .general.menu,\n        size: CGFloat,\n        notification: InboxNotification,\n        type: Set<EllipsisMenuType> = [.basic, .moderator]\n    ) where Content == CustomizableActionMenu<ReplyBarConfiguration> {\n        self.icon = icon\n        self.size = size\n\n        self.content = CustomizableActionMenu(configuration: \\.interactionBar_reply) { seed, _ in\n            if seed.isModeratorAction {\n                if !type.contains(.moderator) { return nil }\n            } else {\n                if !type.contains(.basic) { return nil }\n            }\n\n            return seed.createAction(notification) ?? seed.createAction(notification.content.wrappedValue)\n        }\n    }\n}\n\nextension EllipsisMenu {\n    init(\n        icon: Icon = .general.menu,\n        size: CGFloat,\n        message: any Message,\n        report: Report,\n        type: Set<EllipsisMenuType> = [.basic, .moderator]\n    ) where Content == CustomizableActionMenu<ReplyBarConfiguration> {\n        self.icon = icon\n        self.size = size\n\n        self.content = CustomizableActionMenu(configuration: \\.interactionBar_reply) { seed, _ in\n            if seed.isModeratorAction {\n                if !type.contains(.moderator) { return nil }\n            } else {\n                if !type.contains(.basic) { return nil }\n            }\n\n            return seed.createAction(report) ?? seed.createAction(message)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ContextMenu+Instance.swift",
    "content": "//\n//  ContextMenu+Instance.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-01-15.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\nimport MlemBackend\n\nprivate let seeds: [ActionSeed] = [\n    .visit,\n    .logIn,\n    .signUp,\n    .openInBrowser,\n    .share,\n    .block\n]\n\nextension View {\n    @ViewBuilder\n    func contextMenu(instance: (any InstanceActionProviding)?) -> some View {\n        if let instance {\n            contextMenu {\n                ActionButtons { _ in\n                    seeds.compactMap { $0.createAction(instance) }\n                }\n            }\n        } else {\n            self\n        }\n    }\n}\n\nextension ToolbarEllipsisMenu {\n    init(instance: any InstanceActionProviding) where Content == ActionButtons {\n        self.init {\n            ActionButtons { _ in\n                seeds.compactMap { $0.createAction(instance) }\n            }\n        }\n    }\n}\n\nextension View {\n    @ViewBuilder\n    func contextMenu(instance: any InstanceActionProviding) -> some View {\n        contextMenu {\n            ActionButtons { _ in\n                seeds.compactMap { $0.createAction(instance) }\n            }\n        }\n    }\n}\n\n// MARK: - InstanceActionProviding\n\npublic protocol InstanceActionProviding: Sharable, Blockable {\n    var instanceStub: InstanceStub { get }\n}\n\nextension Instance: InstanceActionProviding {\n    public var instanceStub: InstanceStub { .init(api: api, actorId: actorId) }\n}\n\nextension InstanceSummary: @retroactive Sharable {}\nextension InstanceSummary: @retroactive ActorIdentifiable {}\nextension InstanceSummary: @retroactive Blockable {}\nextension InstanceSummary: InstanceActionProviding {\n    public var actorId: ActorIdentifier { instanceStub.actorId }\n    public func url() -> URL { actorId.url }\n    public var blocked: any RealizedValueProviding<Bool> { RealizedValue(false) }\n    public var updateBlocked: ((Bool, ((Bool) -> Void)?) -> Void)? { nil }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ContextMenu+Message.swift",
    "content": "//\n//  ContextMenu+Message.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-17.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nprivate let seeds: [ActionSeed] = [\n    .reply,\n    .selectText,\n    .report,\n    .edit,\n    .delete\n]\n\nextension View {\n    func contextMenu(message: any Message1Providing) -> some View {\n        contextMenu {\n            ActionButtons { _ in\n                seeds.compactMap { $0.createAction(message) }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ContextMenu+Person.swift",
    "content": "//\n//  ContextMenu+Person.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-17.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nprivate let seeds: [ActionSeed] = [\n    .goToInstance,\n    .copyName,\n    .share,\n    .sendMessage,\n    .block,\n    .editNote,\n    .openModlog,\n    .ban,\n    .purge,\n    .appointModerator,\n    .appointAdmin\n]\n\nextension ActionButtons {\n    init(person: Person) {\n        self.init { _ in\n            seeds.compactMap { $0.createAction(person) }\n        }\n    }\n}\n\nextension View {\n    func contextMenu(person: Person) -> some View {\n        contextMenu {\n            ActionButtons(person: person)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ContextMenu+Post.swift",
    "content": "//\n//  ContextMenu+Post.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-12-23.\n//\n\nimport Actions\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\n\nextension View {\n    func contextMenu(post: Post) -> some View {\n        contextMenu {\n            CustomizableActionMenu(\n                entity: post,\n                configuration: \\.interactionBar_post,\n                modMailConfiguration: \\.interactionBar_postReport,\n                customizable: true\n            )\n        }\n        .popupAnchor()\n    }\n\n    @ViewBuilder\n    func quickSwipes(post: Post, configuration: PostBarConfiguration) -> some View {\n        quickSwipes(\n            leading: configuration.swipes.leading.compactMap { $0.createAction(post) },\n            trailing: configuration.swipes.trailing.compactMap { $0.createAction(post) }\n        )\n    }\n}\n\nenum EllipsisMenuType {\n    case basic, moderator\n}\n\nextension EllipsisMenu {\n    init(\n        icon: Icon = .general.menu,\n        size: CGFloat,\n        post: Post,\n        type: Set<EllipsisMenuType> = [.basic, .moderator]\n    ) where Content == CustomizableActionMenu<PostBarConfiguration> {\n        self.icon = icon\n        self.size = size\n\n        self.content = CustomizableActionMenu(\n            entity: post,\n            configuration: \\.interactionBar_post,\n            modMailConfiguration: \\.interactionBar_postReport,\n            customizable: true\n        ) { seed in\n            if seed.isModeratorAction {\n                return type.contains(.moderator)\n            } else {\n                return type.contains(.basic)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/CopyNameAction.swift",
    "content": "//\n//  CopyNameAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CopyNameAction: Actions.Action {\n    enum Relationship { case identity, author }\n    let text: String\n    let relationship: Relationship\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let copyName = ActionSeed(\n        \"copyName\",\n        label: CopyNameAction.createLabel(relationship: .identity)\n    ) { entity in\n        switch entity {\n        case let entity as Person:\n            CopyNameAction(text: entity.fullNameWithPrefix, relationship: .identity)\n        case let entity as Community:\n            CopyNameAction(text: entity.fullNameWithPrefix, relationship: .identity)\n        default:\n            nil\n        }\n    }\n\n    static let copyAuthorName = ActionSeed(\n        \"copyAuthorName\",\n        label: CopyNameAction.createLabel(relationship: .author)\n    ) { entity in\n        switch entity {\n        case let entity as any InteractableProviding:\n            if let creator = entity.creator.value {\n                CopyNameAction(text: creator.fullNameWithPrefix, relationship: .author)\n            } else {\n                nil\n            }\n        default:\n            nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension CopyNameAction {\n    static func createLabel(relationship: Relationship) -> ActionLabel {\n        .init(\n            relationship == .identity ? \"Copy Name\" : \"Copy Username\",\n            icon: .general.copy,\n            color: .themedColorfulAccent(4)\n        )\n    }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.createLabel(relationship: self.relationship)\n    }\n}\n\n// MARK: - Behavior\n\nextension CopyNameAction {\n    func execute(environment: EnvironmentValues) {\n        environment.toastModel?.add(.success(\"Copied\"))\n        UIPasteboard.general.string = self.text\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/CreateImageAction.swift",
    "content": "//\n//  CreateImageAction.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-12-06.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CreateImageAction: SimpleLabelAction {\n    enum Content {\n        case comment(Comment)\n        case post(Post)\n    }\n    \n    let content: Content\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let createImage = ActionSeed(\"createImage\") { entity in\n        switch entity {\n        case let entity as Post: CreateImageAction(content: .post(entity))\n        case let entity as Comment: CreateImageAction(content: .comment(entity))\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension CreateImageAction {\n    static let label: ActionLabel = .init(\n        \"Create Image\",\n        icon: .general.createImage,\n        color: .themedColorfulAccent(5)\n    )\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.label.withVisibility(visibility(environment))\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        if environment.isContextMenu && environment.feedContext != .post {\n            .hidden\n        } else {\n            .enabled\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension CreateImageAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        guard let navigation = environment.navigation else {\n            assertionFailure()\n            return\n        }\n\n        switch self.content {\n        case let .post(post):\n            navigation.openSheet(.exportPostImage(post))\n        case let .comment(comment):\n            navigation.openSheet(.exportCommentImage(comment, tracker: environment.commentTreeTracker))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/CrosspostAction.swift",
    "content": "//\n//  CrosspostAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-15.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CrosspostAction: SimpleLabelAction {\n    let entity: Post\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let crosspost = ActionSeed(\"crosspost\") { entity in\n        switch entity {\n        case let entity as Post: CrosspostAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension CrosspostAction {\n    static let label: ActionLabel = .init(\n        \"Crosspost\",\n        icon: .lemmy.crosspost,\n        color: .themedColorfulAccent(5)\n    )\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.label.withVisibility(visibility(environment))\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        if entity.api.canInteract(appState: environment.appState) {\n            .enabled\n        } else {\n            .hidden\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension CrosspostAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        var crossPostContent: String\n        let crossPostedLabel = String(localized: \"Crossposted from \\(entity.actorId.description)\")\n        if let content = entity.content, !content.isEmpty {\n            crossPostContent = \"\\(crossPostedLabel)\\n-----\\n\\(content)\"\n        } else {\n            crossPostContent = crossPostedLabel\n        }\n        environment.navigation?.openSheet(.createPost(\n            community: nil,\n            title: entity.title,\n            content: crossPostContent,\n            type: entity.type,\n            nsfw: entity.nsfw,\n            feedLoader: .init(wrappedValue: nil)\n        ))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/DeleteAction.swift",
    "content": "//\n//  DeleteAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-17.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct DeleteAction: SimpleLabelAction {\n    let entity: any DeletableProviding\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let delete = ActionSeed(\"delete\") { entity in\n        switch entity {\n        case let entity as any DeletableProviding: DeleteAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension DeleteAction {\n    static let deleteLabel: ActionLabel = .init(\n        \"Delete\",\n        icon: .general.delete,\n        color: .themedNegative,\n        isDestructive: true\n    )\n    static let restoreLabel: ActionLabel = .init(\n        \"Restore\",\n        icon: .lemmy.restore,\n        color: .themedPositive\n    )\n    \n    static var label: ActionLabel { deleteLabel }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        if entity.deleted {\n            Self.restoreLabel.withVisibility(visibility(environment))\n        } else {\n            Self.deleteLabel.withVisibility(visibility(environment))\n        }\n    }\n    \n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard entity.api.canInteract(appState: environment.appState) else { return .hidden }\n        \n        guard let myPersonId = entity.api.myPerson?.id else { return .hidden }\n        guard entity.isOwnContent(myPersonId: myPersonId) else { return .hidden }\n        guard !entity.deleted || canUndelete else { return .hidden }\n        \n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension DeleteAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.popupModel?.showPopup(message: \"Really delete?\", [\n            .init(title: \"Yes\", isDestructive: true) {\n                entity.toggleDeleted { status in\n                    let toast = createToast(didDelete: entity.deleted, requestStatus: status)\n                    environment.toastModel?.add(toast)\n                }\n            }\n        ])\n    }\n    \n    var canUndelete: Bool {\n        switch entity {\n        case is any Message1Providing:\n            entity.api.supports(.undeletePrivateMessages, defaultValue: true)\n        default:\n            true\n        }\n    }\n    \n    private func createToast(didDelete: Bool, requestStatus: UpdateStatus) -> ToastType {\n        switch (didDelete, requestStatus) {\n        case (true, .success): createConfirmationToast()\n        case (true, .failure): .failure(\"Failed to delete!\")\n        case (false, .success): .success(\"Restored\")\n        case (false, .failure): .failure(\"Failed to restore!\")\n        }\n    }\n    \n    private func createConfirmationToast() -> ToastType {\n        if canUndelete {\n            .undoable(\n                \"Deleted\",\n                icon: .general.delete,\n                callback: { entity.updateDeleted(false, callback: nil) },\n                color: .themedNegative\n            )\n        } else {\n            .basic(\n                \"Deleted\",\n                icon: .general.delete,\n                color: .themedNegative\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/EditAction.swift",
    "content": "//\n//  EditAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-17.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct EditAction: SimpleLabelAction {\n    enum Content {\n        case post(Post)\n        case comment(Comment)\n        case message(any Message1Providing)\n        \n        var value: any OwnershipProviding {\n            switch self {\n            case let .post(post): post\n            case let .comment(comment): comment\n            case let .message(message): message\n            }\n        }\n    }\n    \n    let content: Content\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let edit = ActionSeed(\"edit\") { entity in\n        switch entity {\n        case let entity as any Message1Providing: EditAction(content: .message(entity))\n        case let entity as Comment: EditAction(content: .comment(entity))\n        case let entity as Post: EditAction(content: .post(entity))\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension EditAction {\n    static let label: ActionLabel = .init(\"Edit\", icon: .general.edit)\n    \n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.label.withVisibility(visibility(environment))\n    }\n    \n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard content.value.api.canInteract(appState: environment.appState) else { return .hidden }\n\n        guard let myPersonId = content.value.api.myPerson?.id else { return .hidden }\n        return content.value.isOwnContent(myPersonId: myPersonId) ? .enabled : .hidden\n    }\n}\n\n// MARK: - Behavior\n\nextension EditAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        switch content {\n        case let .comment(comment):\n            environment.navigation?.openSheet(.editComment(comment, context: nil))\n        case let .post(post):\n            environment.navigation?.openSheet(.editPost(post))\n        case let .message(message):\n            if let message = message as? any Message2Providing {\n                if let editMessage = environment.editMessage {\n                    editMessage(message.message2)\n                } else {\n                    let otherPerson = message.isOwnMessage ? message.recipient : message.creator\n                    environment.navigation?.push(.messageFeed(otherPerson, focusTextField: true, editing: message))\n                }\n            } else {\n                assertionFailure()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/EditNoteAction.swift",
    "content": "//\n//  EditNoteAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-12-13.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct EditNoteAction: Actions.Action {\n    let entity: Person\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let editNote = ActionSeed(\n        \"editNote\",\n        label: EditNoteAction.createLabel(noteExists: true)\n    ) { entity in\n        switch entity {\n        case let entity as Person: EditNoteAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension EditNoteAction {\n    static func createLabel(noteExists: Bool) -> ActionLabel {\n        if noteExists {\n            .init(\"Edit Note\", icon: .lemmy.editNote)\n        } else {\n            .init(\"Add Note\", icon: .lemmy.editNote)\n        }\n    }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.createLabel(noteExists: entity.note != nil)\n            .withVisibility(entity.api.supports(.userNotes, defaultValue: false) ? .enabled : .hidden)\n    }\n}\n\n// MARK: - Behavior\n\nextension EditNoteAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.navigation?.openSheet(.editNote(entity))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/FavoriteAction.swift",
    "content": "//\n//  FavoriteAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-02-08.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct FavoriteAction: SimpleLabelAction {\n    let entity: Community\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let favorite = ActionSeed(\"favorite\") { entity in\n        switch entity {\n        case let entity as Community: FavoriteAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension FavoriteAction {\n    static let favoriteLabel: ActionLabel = .init(\n        \"Favorite\",\n        icon: .lemmy.favorite,\n        color: .themedFavorite\n    )\n    static let unfavoriteLabel: ActionLabel = .init(\n        \"Unfavorite\",\n        icon: .lemmy.unfavorite,\n        color: .themedFavorite\n    )\n    \n    static var label: ActionLabel { favoriteLabel }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        if entity.favorited {\n            return Self.unfavoriteLabel.withVisibility(visibility(environment))\n        } else {\n            return Self.favoriteLabel.withVisibility(visibility(environment))\n        }\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard entity.api.canInteract(appState: environment.appState),\n              entity.updateFavorite != nil else { return .hidden }\n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension FavoriteAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        guard let updateFavorite = entity.updateFavorite else { return }\n        environment.hapticManager.play(haptic: .lightSuccess, tier: .low)\n        if entity.favorited {\n            environment.toastModel?.add(\n                .undoable(\n                    \"Unfavorited\",\n                    icon: .lemmy.unfavorite,\n                    callback: {\n                        updateFavorite(true)\n                    },\n                    color: .themedFavorite\n                )\n            )\n        } else {\n            environment.toastModel?.add(\n                .basic(\"Favorited\", icon: .lemmy.favorite, color: .themedFavorite)\n            )\n        }\n        updateFavorite(!entity.favorited)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/GoToInstanceAction.swift",
    "content": "//\n//  GoToInstanceAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct GoToInstanceAction: SimpleLabelAction {\n    let entity: any ActorIdentifiable\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let goToInstance = ActionSeed(\"goToInstance\") { entity in\n        switch entity {\n        case let entity as any ActorIdentifiable: GoToInstanceAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension GoToInstanceAction {\n    static let label: ActionLabel = .init(\n        \"Go to Instance\",\n        icon: .lemmy.instance,\n        color: .themedColorfulAccent(1)\n    )\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.label.withTitle(entity.host)\n    }\n}\n\n// MARK: - Behavior\n\nextension GoToInstanceAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.navigation?.push(.hostInstance(of: self.entity))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/HideAction.swift",
    "content": "//\n//  HideAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-12-23.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct HideAction: SimpleLabelAction {\n    let entity: Post\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let hide = ActionSeed(\"hide\") { entity in\n        switch entity {\n        case let entity as Post: HideAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension HideAction {\n    static let hideLabel: ActionLabel = .init(\n        \"Hide\",\n        icon: .general.hide,\n        color: .themedColorfulAccent(4)\n    )\n\n    static let showLabel: ActionLabel = .init(\n        \"Show\",\n        icon: .general.show,\n        color: .themedColorfulAccent(4)\n    )\n    \n    static var label: ActionLabel { hideLabel }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        guard let hidden = entity.hidden.value else { return Self.showLabel.withVisibility(.hidden) }\n        if hidden {\n            return Self.showLabel.withVisibility(visibility(environment))\n        } else {\n            return Self.hideLabel.withVisibility(visibility(environment))\n        }\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        if entity.api.canInteract(appState: environment.appState),\n            entity.api.supports(.hidePosts, defaultValue: false) {\n            .enabled\n        } else {\n            .hidden\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension HideAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        guard let hidden = entity.hidden.value, let toggleHidden = entity.toggleHidden else { return }\n        toggleHidden([])\n        environment.hapticManager.play(haptic: .lightSuccess, tier: .low)\n        if !hidden {\n            environment.toastModel?.add(\n                .undoable(\n                    \"Hidden\",\n                    icon: .general.hide,\n                    callback: {\n                        entity.updateHidden(false)\n                    }\n                )\n            )\n        } else {\n            environment.toastModel?.add(.success(\"Shown\"))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/LockAction.swift",
    "content": "//\n//  LockAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-12-23.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct LockAction: SimpleLabelAction {\n    let entity: Post\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let lock = ActionSeed(\"lock\") { entity in\n        switch entity {\n        case let entity as Post: LockAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension LockAction {\n    static let lockLabel: ActionLabel = .init(\n        \"Lock\",\n        icon: .lemmy.addLock,\n        color: .themedLockAccent\n    )\n\n    static let unlockLabel: ActionLabel = .init(\n        \"Unlock\",\n        icon: .lemmy.removeLock,\n        color: .themedLockAccent\n    )\n    \n    static var label: ActionLabel { lockLabel }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        if entity.locked {\n            Self.unlockLabel.withVisibility(visibility(environment))\n        } else {\n            Self.lockLabel.withVisibility(visibility(environment))\n        }\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        if entity.api.canInteract(appState: environment.appState), entity.canModerate {\n            return .enabled\n        } else {\n            return .hidden\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension LockAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.popupModel?.showPopup(\n            message: entity.locked ? \"Really unlock this post?\" : \"Really lock this post?\",\n            [\n            .init(title: \"Yes\", isDestructive: true) {\n                let shouldLock = !entity.locked\n                entity.toggleLocked([]) { status in\n                    self.handleResult(\n                        status: status,\n                        shouldLock: shouldLock,\n                        environment: environment\n                    )\n                }\n            }\n        ])\n    }\n\n    @MainActor\n    func handleResult(\n        status: UpdateStatus,\n        shouldLock: Bool,\n        environment: EnvironmentValues\n    ) {\n        switch status {\n        case .success:\n            environment.hapticManager.play(haptic: .lightSuccess, tier: .low)\n        case .failure: \n            environment.toastModel?.add(\n                .failure(shouldLock ? \"Failed to lock post\" : \"Failed to unlock post\")\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/LogInAction.swift",
    "content": "//\n//  LogInAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-01-16.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct LogInAction: SimpleLabelAction {\n    let instance: any InstanceActionProviding\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let logIn = ActionSeed(\"logIn\") { entity in\n        switch entity {\n        case let entity as any InstanceActionProviding: LogInAction(instance: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension LogInAction {\n    static let label: ActionLabel = .init(\"Log In\", icon: .lemmy.logIn)\n}\n\n// MARK: - Behavior\n\nextension LogInAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        if let instance = instance as? Instance {\n            environment.navigation?.openSheet(.logIn(.instance(instance)))\n        } else {\n            environment.navigation?.openSheet(.instanceStub(instance.instanceStub) { .logIn(.instance($0)) })\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/MarkNsfwAction.swift",
    "content": "//\n//  MarkNsfwAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-12-23.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct MarkNsfwAction: SimpleLabelAction {\n    let entity: Post\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let markNsfw = ActionSeed(\"markNsfw\") { entity in\n        switch entity {\n        case let entity as Post: MarkNsfwAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension MarkNsfwAction {\n    static let addLabel: ActionLabel = .init(\n        \"Add NSFW Tag\",\n        icon: .settings.blurNsfw,\n        color: .themedNegative\n    )\n\n    static let removeLabel: ActionLabel = .init(\n        \"Remove NSFW Tag\",\n        icon: .settings.blurNsfw,\n        color: .themedNegative\n    )\n    \n    static var label: ActionLabel { addLabel }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        if entity.nsfw {\n            Self.removeLabel.withVisibility(visibility(environment))\n        } else {\n            Self.addLabel.withVisibility(visibility(environment))\n        }\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        if entity.api.canInteract(appState: environment.appState),\n           entity.canModerate,\n           let community = entity.community.value,\n           community.apiIsLocal, // Setting NSFW doesn't work on non-local communities at the time of writing\n           entity.api.supports(.moderatorSetNsfw, defaultValue: false) {\n            return .enabled\n        } else {\n            return .hidden\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension MarkNsfwAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.popupModel?.showPopup(\n            message: entity.nsfw ? \"Really remove NSFW tag?\" : \"Really add NSFW tag?\",\n            [\n            .init(title: \"Yes\", isDestructive: true) {\n                entity.toggleNsfw { status in\n                    switch status {\n                    case .success:\n                        environment.hapticManager.play(haptic: .lightSuccess, tier: .low)\n                    case .failure: \n                        environment.toastModel?.add(.failure(\"Failed to set NSFW status\"))\n                    }\n                }\n            }\n        ])\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/MarkReadAction.swift",
    "content": "//\n//  MarkReadAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-11-07.\n//  \n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct MarkReadAction: SimpleLabelAction {\n    let notification: InboxNotification\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let markRead = ActionSeed(\"markRead\") { entity in\n        switch entity {\n        case let entity as InboxNotification: MarkReadAction(notification: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension MarkReadAction {\n    static let markReadLabel: ActionLabel = .init(\n        \"Mark Read\",\n        icon: .lemmy.markRead,\n        color: .themedRead\n    )\n    static let markUnreadLabel: ActionLabel = .init(\n        \"Mark Unread\",\n        icon: .lemmy.markUnread,\n        color: .themedRead\n    )\n    \n    static var label: ActionLabel { markReadLabel }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        if notification.read {\n            Self.markUnreadLabel.withVisibility(visibility(environment))\n        } else {\n            Self.markReadLabel.withVisibility(visibility(environment))\n        }\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard notification.api.canInteract(appState: environment.appState) else { return .hidden }\n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension MarkReadAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        notification.toggleRead()\n        environment.hapticManager.play(haptic: .lightSuccess, tier: .low)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/NewPostAction.swift",
    "content": "//\n//  NewPostAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-02-08.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct NewPostAction: SimpleLabelAction {\n    let entity: Community\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let newPost = ActionSeed(\"newPost\") { entity in\n        switch entity {\n        case let entity as Community: NewPostAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension NewPostAction {\n    static let label: ActionLabel = .init(\"New Post\", icon: .lemmy.send)\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        if entity.api.canInteract(appState: environment.appState) {\n            Self.label.withVisibility(.enabled)\n        } else {\n            Self.label.withVisibility(.disabled)\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension NewPostAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.navigation?.openSheet(.createPost(community: self.entity, type: nil, feedLoader: environment.feedLoader)) \n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/OpenInBrowserAction.swift",
    "content": "//\n//  OpenInBrowserAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-01-16.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct OpenInBrowserAction: SimpleLabelAction {\n    let url: URL\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let openInBrowser = ActionSeed(\"openInBrowser\") { entity in\n        switch entity {\n        case let entity as any Sharable: OpenInBrowserAction(url: entity.url())\n        case let entity as any InstanceActionProviding: OpenInBrowserAction(url: entity.actorId.url)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension OpenInBrowserAction {\n    static let label: ActionLabel = .init(\"Open in Browser\", icon: .general.browser)\n}\n\n// MARK: - Behavior\n\nextension OpenInBrowserAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        openLinkAsWebsite(url: url)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/OpenModlogAction.swift",
    "content": "//\n//  OpenModlogAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct OpenModlogAction: Actions.Action {\n    enum Content {\n        case person(Person)\n    }\n\n    enum Relationship { case identity, author }\n    \n    let content: Content\n    let relationship: Relationship\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let openModlog = ActionSeed(\n        \"openModlog\",\n        label: OpenModlogAction.createLabel(relationship: .identity)\n    ) { entity in\n        switch entity {\n        case let entity as Person: OpenModlogAction(content: .person(entity), relationship: .identity)\n        default: nil\n        }\n    }\n\n    static let openCreatorModlog = ActionSeed(\n        \"openCreatorModlog\",\n        label: OpenModlogAction.createLabel(relationship: .author)\n    ) { entity in\n        switch entity {\n        case let entity as any InteractableProviding:\n            if let creator = entity.creator.value {\n                OpenModlogAction(content: .person(creator), relationship: .author)\n            } else {\n                nil\n            }\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension OpenModlogAction {\n    static func createLabel(relationship: Relationship) -> ActionLabel {\n        .init(\n            relationship == .identity ? \"Modlog\" : \"User Modlog\",\n            icon: .lemmy.modlog,\n            color: .themedModeration\n        )\n    }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        return Self.createLabel(relationship: relationship)\n    }\n}\n\n// MARK: - Behavior\n\nextension OpenModlogAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        switch content {\n        case let .person(person):\n            execute(person: person, environment: environment)\n        }\n    }\n\n    @MainActor\n    private func execute(person: Person, environment: EnvironmentValues) {\n        environment.popupModel?.showPopup(message: \"Filter as...\", [\n            .init(title: \"Subject\") {\n                environment.navigation?.push(.modlog(targetPerson: .init(person), moderatorPerson: nil))\n            },\n            .init(title: \"Moderator\") {\n                environment.navigation?.push(.modlog(targetPerson: nil, moderatorPerson: .init(person)))\n            }\n        ])\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/PinAction.swift",
    "content": "//\n//  PinAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-12-23.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct PinAction: SimpleLabelAction {\n    let entity: Post\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let pin = ActionSeed(\"pin\") { entity in\n        switch entity {\n        case let entity as Post: PinAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension PinAction {\n    static let pinLabel: ActionLabel = .init(\n        \"Pin\",\n        icon: .lemmy.addPin,\n        color: .themedModeration\n    )\n\n    static let unpinLabel: ActionLabel = .init(\n        \"Unpin\",\n        icon: .lemmy.removePin,\n        color: .themedModeration\n    )\n\n    static let pinDetailsLabel: ActionLabel = .init(\n        \"Pin...\",\n        icon: .lemmy.addPin,\n        color: .themedModeration\n    )\n    \n    static var label: ActionLabel { pinLabel }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        let label: ActionLabel = if entity.api.isAdmin {\n            switch (entity.pinnedInstance, entity.pinnedCommunity) {\n            case (true, true):\n                Self.unpinLabel\n            case (true, false), (false, true):\n                Self.pinDetailsLabel\n            case (false, false):\n                Self.pinLabel\n            }\n        } else {\n            if entity.pinnedCommunity {\n                Self.unpinLabel\n            } else {\n                Self.pinLabel\n            }\n        }\n\n        return label.withVisibility(visibility(environment))\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        if entity.api.canInteract(appState: environment.appState), entity.canModerate {\n            .enabled\n        } else {\n            .hidden\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension PinAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        if entity.api.isAdmin {\n            executeAsAdmin(environment: environment)\n        } else {\n            executeAsModerator(environment: environment)\n        }\n    }\n\n    @MainActor\n    func executeAsModerator(environment: EnvironmentValues) {\n        environment.popupModel?.showPopup(\n            message: entity.pinnedCommunity ? \"Really unpin this post?\" : \"Really pin this post?\",\n            [\n            .init(title: \"Yes\", isDestructive: false) {\n                togglePinnedCommunity(environment: environment)\n            }\n        ])\n    }\n\n    @MainActor\n    func executeAsAdmin(environment: EnvironmentValues) {\n        environment.popupModel?.showPopup(\n            message: \"Choose target...\",\n            [\n                .init(title: entity.pinnedCommunity ? \"Unpin from community\" : \"Pin to community\") {\n                    togglePinnedCommunity(environment: environment)\n                },\n                .init(title: entity.pinnedInstance ? \"Unpin from instance\" : \"Pin to instance\") {\n                    togglePinnedInstance(environment: environment)\n                }\n            ]\n        )\n    }\n\n    @MainActor\n    func togglePinnedCommunity(environment: EnvironmentValues) {\n        let shouldPin = entity.pinnedCommunity\n        entity.togglePinnedCommunity { status in\n            handleResult(\n                status: status,\n                shouldPin: shouldPin,\n                environment: environment\n            )\n        }\n    }\n\n    @MainActor\n    func togglePinnedInstance(environment: EnvironmentValues) {\n        let shouldPin = entity.pinnedInstance\n        entity.togglePinnedInstance { status in\n            handleResult(\n                status: status,\n                shouldPin: shouldPin,\n                environment: environment\n            )\n        }\n    }\n\n    @MainActor\n    func handleResult(\n        status: UpdateStatus,\n        shouldPin: Bool,\n        environment: EnvironmentValues\n    ) {\n        switch status {\n        case .success:\n            environment.hapticManager.play(haptic: .lightSuccess, tier: .low)\n        case .failure: \n            environment.toastModel?.add(\n                .failure(shouldPin ? \"Failed to pin post\" : \"Failed to unpin post\")\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/PurgeAction.swift",
    "content": "//\n//  PurgeAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct PurgeAction: Actions.Action {\n    enum Relationship { case identity, author }\n\n    let entity: any PurgableProviding\n    let relationship: Relationship\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let purge = ActionSeed(\"purge\", label: PurgeAction.createLabel(relationship: .identity)) { entity in\n        switch entity {\n        case let entity as any PurgableProviding: PurgeAction(entity: entity, relationship: .identity)\n        default: nil\n        }\n    }\n\n    static let purgeCreator = ActionSeed(\"purgeCreator\", label: PurgeAction.createLabel(relationship: .author)) { entity in\n        switch entity {\n        case let entity as any InteractableProviding:\n            if let creator = entity.creator.value {\n                PurgeAction(entity: creator, relationship: .author)\n            } else {\n                nil\n            }\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension PurgeAction {\n    static func createLabel(relationship: Relationship) -> ActionLabel {\n        .init(\n            relationship == .identity ? \"Purge\" : \"Purge User\",\n            icon: .lemmy.purge,\n            color: .themedNegative,\n            isDestructive: true\n        )\n    }\n    \n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        return Self.createLabel(relationship: self.relationship).withVisibility(visibility(environment))\n    }\n    \n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard entity.api.canInteract(appState: environment.appState) else { return .hidden }\n        \n        guard entity.api.supports(.purgeContent, defaultValue: false) else { return .hidden }\n        guard entity.api.isAdmin else { return .hidden }\n\n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension PurgeAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.navigation?.openSheet(.purge(entity))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/RemoveAction.swift",
    "content": "//\n//  RemoveAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct RemoveAction: SimpleLabelAction {\n    let entity: any RemovableProviding\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let remove = ActionSeed(\"remove\") { entity in\n        switch entity {\n        case let entity as any RemovableProviding: RemoveAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension RemoveAction {\n    static let removeLabel: ActionLabel = .init(\n        \"Remove\",\n        icon: .lemmy.remove,\n        color: .themedNegative,\n        isDestructive: true\n    )\n    static let restoreLabel: ActionLabel = .init(\n        \"Restore\",\n        icon: .lemmy.restore,\n        color: .themedPositive\n    )\n    \n    static var label: ActionLabel { removeLabel }\n    \n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        if entity.removed {\n            Self.restoreLabel.withVisibility(visibility(environment))\n        } else {\n            Self.removeLabel.withVisibility(visibility(environment))\n        }\n    }\n    \n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard entity.api.canInteract(appState: environment.appState) else { return .hidden }\n        \n        guard let myPerson = entity.api.myPerson else { return .hidden }\n        guard entity.canModerate else { return .hidden }\n\n        if let entity = entity as? any OwnershipProviding {\n            guard !entity.isOwnContent(myPersonId: myPerson.id) else { return .hidden }\n        }\n\n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension RemoveAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.navigation?.openSheet(.remove(entity))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ReplyAction.swift",
    "content": "//\n//  ReplyAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ReplyAction: SimpleLabelAction {\n    enum Content {\n        case post(Post)\n        case comment(Comment)\n        case message(any Message2Providing)\n        \n        var value: any OwnershipProviding {\n            switch self {\n            case let .post(post): post\n            case let .comment(comment): comment\n            case let .message(message): message\n            }\n        }\n    }\n    \n    let content: Content\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let reply = ActionSeed(\"reply\") { entity in\n        switch entity {\n        case let entity as Post: ReplyAction(content: .post(entity))\n        case let entity as Comment: ReplyAction(content: .comment(entity))\n        case let entity as any Message2Providing: ReplyAction(content: .message(entity))\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension ReplyAction {\n    static let label: ActionLabel = .init(\"Reply\", icon: .lemmy.reply)\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.label.withVisibility(visibility(environment))\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard content.value.api.canInteract(appState: environment.appState) else { return .hidden }\n\n        // Don't show the reply action for messages in the message feed\n        if case .message = self.content, case .messageFeed = environment.navigation?.path.last {\n            return .hidden\n        }\n\n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension ReplyAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        guard let navigation = environment.navigation else {\n            assertionFailure()\n            return\n        }\n\n        switch self.content {\n        case let .post(post):\n            navigation.openSheet(.createComment(.post(post), commentTreeTracker: environment.commentTreeTracker))\n        case let .comment(comment):\n            navigation.openSheet(.createComment(.comment(comment), commentTreeTracker: environment.commentTreeTracker))\n        case let .message(message):\n            navigation.push(.messageFeed(message.creator, focusTextField: true))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ReportAction.swift",
    "content": "//\n//  ReportAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ReportAction: SimpleLabelAction {\n    let entity: any ReportableProviding\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let report = ActionSeed(\"report\") { entity in\n        switch entity {\n        case let entity as any ReportableProviding: ReportAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension ReportAction {\n    static let label: ActionLabel = .init(\n        \"Report\",\n        icon: .lemmy.report,\n        color: .themedNegative,\n        isDestructive: true\n    )\n    \n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.label.withVisibility(visibility(environment))\n    }\n    \n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard entity.api.canInteract(appState: environment.appState) else { return .hidden }\n        \n        guard let myPersonId = entity.api.myPerson?.id else { return .hidden }\n        if entity.isOwnContent(myPersonId: myPersonId) { return .hidden }\n        \n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension ReportAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.navigation?.openSheet(.report(entity, community: environment.communityContext))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ResolveAction.swift",
    "content": "//\n//  ResolveAction.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-12-24.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ResolveAction: SimpleLabelAction {\n    let entity: Report\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let resolveReport = ActionSeed(\"resolveReport\") { entity in\n        switch entity {\n        case let entity as Report: ResolveAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension ResolveAction {\n    static let resolveLabel: ActionLabel = .init(\n        \"Resolve\",\n        icon: .init(\"checkmark.circle\"),\n        color: .themedPositive\n    )\n    \n    static let unresolveLabel: ActionLabel = .init(\n        \"Unresolve\",\n        icon: .init(\"xmark.circle\"),\n        color: .themedNegative\n    )\n    \n    static var label: ActionLabel { resolveLabel }\n    \n    func createLabel(environment: EnvironmentValues) -> Actions.ActionLabel {\n        entity.resolved ? Self.unresolveLabel : Self.resolveLabel\n    }\n    \n    func execute(environment: EnvironmentValues) {\n        entity.toggleResolved()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/SaveAction.swift",
    "content": "//\n//  SaveAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct SaveAction: SimpleLabelAction {\n    let entity: any InteractableProviding\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let save = ActionSeed(\"save\") { entity in\n        switch entity {\n        case let entity as any InteractableProviding: SaveAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension SaveAction {\n    static let saveLabel: ActionLabel = .init(\n        \"Save\",\n        icon: .lemmy.saved.representingState(active: false),\n        color: .themedSave\n    )\n    static let unsaveLabel: ActionLabel = .init(\n        \"Saved\",\n        icon: .lemmy.saved.representingState(active: true),\n        color: .themedSave\n    )\n    \n    static var label: ActionLabel { saveLabel }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        guard let saved = entity.saved.value else { return Self.saveLabel.withVisibility(.hidden) }\n        if saved {\n            return Self.unsaveLabel.withVisibility(visibility(environment))\n        } else {\n            return Self.saveLabel.withVisibility(visibility(environment))\n        }\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard entity.api.canInteract(appState: environment.appState) else { return .hidden }\n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension SaveAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        guard let toggleSaved = entity.toggleSaved else { return }\n        toggleSaved([.haptic])\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/SelectTextAction.swift",
    "content": "//\n//  SelectTextAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct SelectTextAction: Actions.SimpleLabelAction {\n    static let label: ActionLabel = .init(\"Select Text\", icon: .general.select)\n    \n    let text: String\n    \n    func execute(environment: EnvironmentValues) {\n        environment.navigation?.openSheet(.selectText(text))\n    }\n}\n\nextension ActionSeed {\n    static let selectText = ActionSeed(\"selectText\") { entity in\n        switch entity {\n        case let entity as any Message1Providing:\n            SelectTextAction(text: entity.content)\n        case let entity as Comment:\n            SelectTextAction(text: entity.content)\n        case let entity as Post:\n            SelectTextAction(text: entity.selectableContent ?? \"\")\n        default:\n            nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/SendMessageAction.swift",
    "content": "//\n//  SendMessageAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct SendMessageAction: SimpleLabelAction {\n    enum Relationship { case identity, author }\n\n    let entity: Person\n    let relationship: Relationship\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let sendMessage = ActionSeed(\"sendMessage\") { entity in\n        switch entity {\n        case let entity as Person: SendMessageAction(entity: entity, relationship: .identity)\n        default: nil\n        }\n    }\n\n    static let sendCreatorMessage = ActionSeed(\"sendCreatorMessage\") { entity in\n        switch entity {\n        case let entity as any InteractableProviding:\n            if let creator = entity.creator.value {\n                SendMessageAction(entity: creator, relationship: .author)\n            } else {\n                nil\n            }\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension SendMessageAction {\n    static let label: ActionLabel = .init(\"Send Message\", icon: .lemmy.message)\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.label.withVisibility(visibility(environment))\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        if environment.appState.firstPerson?.actorId == entity.actorId {\n            return .hidden\n        }\n        if environment.isInMessageFeed {\n            return .hidden\n        }\n        if !entity.api.canInteract(appState: environment.appState) {\n            return .disabled\n        }\n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension SendMessageAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.navigation?.openSheet(.messageFeed(self.entity, focusTextField: true)) \n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ShareAction.swift",
    "content": "//\n//  ShareAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ShareAction: SimpleLabelAction {\n    let entity: any Sharable\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let share = ActionSeed(\"share\") { entity in\n        switch entity {\n        case let entity as any Sharable: ShareAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension ShareAction {\n    static let label: ActionLabel = .init(\n        \"Share...\",\n        icon: .general.share,\n        color: .themedColorfulAccent(3)\n    )\n}\n\n// MARK: - Behavior\n\nextension ShareAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        let url: URL? = switch Settings.get(\\.links_shareMode) {\n        case .myInstance: entity.url()\n        case .originalInstance: entity.actorId.url\n        case .lemmyverse: entity.lemmyverseUrl\n        case .askEveryTime: nil\n        }\n        if let url, let navigation = environment.navigation {\n            if case .actionSheet = navigation.root {\n                navigation.dismissSheet()\n                DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {\n                    NavigationModel.main.shareInfo = .init(url: url, actions: entity.shareSheetActions())\n                }\n            } else {\n                navigation.model?.shareInfo = .init(url: url, actions: entity.shareSheetActions())\n            }\n        } else {\n            environment.navigation?.openSheet(.shareInstancePicker(entity))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/SignUpAction.swift",
    "content": "//\n//  SignUpAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-01-16.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct SignUpAction: SimpleLabelAction {\n    let instance: InstanceStub\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let signUp = ActionSeed(\"signUp\") { entity in\n        switch entity {\n        case let entity as any InstanceActionProviding: SignUpAction(instance: entity.instanceStub)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension SignUpAction {\n    static let label: ActionLabel = .init(\"Sign Up\", icon: .lemmy.signUp)\n}\n\n// MARK: - Behavior\n\nextension SignUpAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.navigation?.openSheet(.signUp(instance))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/SubscribeAction.swift",
    "content": "//\n//  SubscribeAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-02-08.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct SubscribeAction: SimpleLabelAction {\n    let entity: Community\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let subscribe = ActionSeed(\"subscribe\") { entity in\n        switch entity {\n        case let entity as Community: SubscribeAction(entity: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension SubscribeAction {\n    static let subscribeLabel: ActionLabel = .init(\n        \"Subscribe\",\n        icon: .lemmy.subscribe,\n        color: .themedPositive\n    )\n    static let unsubscribeLabel: ActionLabel = .init(\n        \"Unsubscribe\",\n        icon: .lemmy.unsubscribe,\n        color: .themedNegative\n    )\n    \n    static var label: ActionLabel { subscribeLabel }\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        guard let subscription = entity.subscription.value  else {\n            return Self.subscribeLabel.withVisibility(.hidden)\n        }\n        if subscription.subscribed {\n            return Self.unsubscribeLabel.withVisibility(visibility(environment))\n        } else {\n            return Self.subscribeLabel.withVisibility(visibility(environment))\n        }\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard entity.api.canInteract(appState: environment.appState),\n              entity.subscription.value != nil,\n              entity.updateSubscribed != nil else { return .hidden }\n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension SubscribeAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        guard let updateSubscribed = entity.updateSubscribed,\n              let subscription = entity.subscription.value,\n              let updateFavorite = entity.updateFavorite else { return }\n        environment.hapticManager.play(haptic: .lightSuccess, tier: .low)\n        let wasFavorited = entity.favorited\n        if subscription.subscribed {\n            environment.toastModel?.add(\n                .undoable(\n                    \"Unsubscribed\",\n                    icon: .lemmy.didUnsubscribe,\n                    callback: {\n                        if wasFavorited {\n                            updateFavorite(true)\n                        } else {\n                            updateSubscribed(true)\n                        }\n                    },\n                    color: .themedAccent\n                )\n            )\n        }\n        updateSubscribed(!subscription.subscribed)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/ViewVotesAction.swift",
    "content": "//\n//  ViewVotesAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ViewVotesAction: SimpleLabelAction {\n    let content: VotesListView.Target\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let viewVotes = ActionSeed(\"viewVotes\") { entity in\n        switch entity {\n        case let entity as Post: ViewVotesAction(content: .post(entity))\n        case let entity as Comment: ViewVotesAction(content: .comment(entity))\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension ViewVotesAction {\n    static let label: ActionLabel = .init(\n        \"View Votes\",\n        icon: .lemmy.votes,\n        color: .themedColorfulAccent(4)\n    )\n    \n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.label.withVisibility(visibility(environment))\n    }\n    \n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        let entity = content.model\n\n        guard entity.api.canInteract(appState: environment.appState) else { return .hidden }\n        \n        guard let myPerson = entity.api.myPerson,\n              let community = entity.community.value,\n              let myPersonModerates = myPerson.moderates,\n              myPersonModerates(.id(community.id)),\n              entity.api.supports(.viewVotes, defaultValue: true) else { return .hidden }\n\n        return .enabled\n    }\n}\n\n// MARK: - Behavior\n\nextension ViewVotesAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        environment.navigation?.openSheet(.votesList(content))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/VisitAction.swift",
    "content": "//\n//  VisitAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-01-16.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct VisitAction: SimpleLabelAction {\n    let instance: any InstanceActionProviding\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let visit = ActionSeed(\"visit\") { entity in\n        switch entity {\n        case let entity as any InstanceActionProviding: VisitAction(instance: entity)\n        default: nil\n        }\n    }\n}\n\n// MARK: - Appearance\n\nextension VisitAction {\n    static let label: ActionLabel = .init(\"Visit\", icon: .lemmy.visitInstance)\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        let api = environment.appState.firstApi\n        let isVisiting = api.host == instance.actorId.host && api.token == nil\n\n        return Self.label.withVisibility(isVisiting ? .disabled : .enabled)\n    }\n}\n\n// MARK: - Behavior\n\nextension VisitAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        do {\n            let account = try GuestAccount.getGuestAccount(url: instance.actorId.url)\n            environment.appState.changeAccount(to: account)\n            environment.appState.contentViewTab = .feeds\n        } catch {\n            handleError(error)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Actions/VoteAction.swift",
    "content": "//\n//  VoteAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-25.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\nstruct VoteAction: Actions.Action {\n    let entity: any InteractableProviding\n    let type: ScoringOperation\n}\n\n// MARK: - Configurability\n\nextension ActionSeed {\n    static let upvote = ActionSeed(\"upvote\", label: VoteAction.upvoteLabel) {\n        createVoteAction($0, type: .upvote)\n    }\n    static let downvote = ActionSeed(\"downvote\", label: VoteAction.downvoteLabel) {\n        createVoteAction($0, type: .downvote)\n    }\n}\n\nprivate func createVoteAction(_ entity: Any, type: ScoringOperation) -> VoteAction? {\n    switch entity {\n    case let entity as any InteractableProviding: VoteAction(entity: entity, type: type)\n    default: nil\n    }\n}\n\n// MARK: - Appearance\n\nextension VoteAction {\n    static let upvoteLabel: ActionLabel = .init(\n        \"Upvote\",\n        icon: .lemmy.upvoted.representingState(active: false),\n        color: .themedUpvote\n    )\n    static let downvoteLabel: ActionLabel = .init(\n        \"Downvote\",\n        icon: .lemmy.downvoted.representingState(active: false),\n        color: .themedDownvote\n    )\n    static let removeUpvoteLabel: ActionLabel = .init(\n        \"Upvoted\",\n        icon: .lemmy.upvoted.representingState(active: true),\n        color: .themedUpvote\n    )\n    static let removeDownvoteLabel: ActionLabel = .init(\n        \"Downvoted\",\n        icon: .lemmy.downvoted.representingState(active: true),\n        color: .themedDownvote\n    )\n\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        guard let votes = entity.votes.value else { return Self.upvoteLabel.withVisibility(.hidden) }\n        let hasMatchingVote = votes.myVote == type\n\n        guard type != .none else {\n            assertionFailure()\n            return Self.upvoteLabel\n        }\n\n        return switch (type, hasMatchingVote) {\n        case (.upvote, false): Self.upvoteLabel\n        case (.upvote, true): Self.removeUpvoteLabel\n        case (.downvote, false): Self.downvoteLabel\n        case (.downvote, true): Self.removeDownvoteLabel\n        default: Self.upvoteLabel\n        }\n    }\n\n    private func visibility(_ environment: EnvironmentValues) -> ActionVisiblity {\n        guard entity.api.canInteract(appState: environment.appState) else { return .hidden }\n\n        let voteFederationMode = entity.api.voteFederationMode\n\n        switch (self.type, entity is Post) {\n        case (.upvote, true):\n            return voteFederationMode.postUpvote == .all ? .enabled : .hidden\n        case (.downvote, true):\n            return voteFederationMode.postDownvote == .all ? .enabled : .hidden\n        case (.upvote, false):\n            return voteFederationMode.commentUpvote == .all ? .enabled : .hidden\n        case (.downvote, false):\n            return voteFederationMode.commentDownvote == .all ? .enabled : .hidden\n        default:\n            assertionFailure()\n            return .hidden\n        }\n    }\n}\n\n// MARK: - Behavior\n\nextension VoteAction {\n    @MainActor\n    func execute(environment: EnvironmentValues) {\n        guard let toggleVote = entity.toggleVote else { return }\n        toggleVote(type, [.haptic])\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/Colors/Palette+Dracula.swift",
    "content": "//\n//  Palette+Dracula.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-08.\n//\n\nimport Foundation\nimport SwiftUI\nimport Theming\n\n// source: https://draculatheme.com/contribute#color-palette\nprivate let _darkBackground: Color = .init(red: 0.07843137255, green: 0.07450980392, blue: 0.1215686275)\nprivate let _background: Color = .init(red: 0.1568627450980392, green: 0.16470588235294117, blue: 0.21176470588235294)\nprivate let _secondaryBackground: Color = .init(red: 53 / 255, green: 55 / 255, blue: 74 / 255) \nprivate let _primary: Color = .init(red: 0.9725490196078431, green: 0.9725490196078431, blue: 0.9490196078431372)\nprivate let _secondary: Color = .init(red: 153 / 255, green: 178 / 255, blue: 255 / 255, opacity: 0.7) \nprivate let _cyan: Color = .init(red: 0.5450980392156862, green: 0.9137254901960784, blue: 0.9921568627450981)\nprivate let _green: Color = .init(red: 0.3137254901960784, green: 0.9803921568627451, blue: 0.4823529411764706)\nprivate let _orange: Color = .init(red: 1.0, green: 0.7215686274509804, blue: 0.4235294117647059)\nprivate let _pink: Color = .init(red: 1.0, green: 0.4745098039215686, blue: 0.7764705882352941)\nprivate let _purple: Color = .init(red: 0.7411764705882353, green: 0.5764705882352941, blue: 0.9764705882352941)\nprivate let _red: Color = .init(red: 1.0, green: 0.3333333333333333, blue: 0.3333333333333333)\nprivate let _yellow: Color = .init(red: 0.9450980392156862, green: 0.9803921568627451, blue: 0.5490196078431373)\n\nextension Palette {\n    static let dracula: Self = .init(\n        bordered: false,\n        label: .init(\n            primary: _primary,\n            secondary: _secondary,\n            tertiary: _secondary\n        ),\n        background: .init(\n            primary: _background,\n            secondary: _secondaryBackground,\n            tertiary: _secondaryBackground\n        ),\n        groupedBackground: .init(\n            primary: _darkBackground,\n            secondary: _background,\n            tertiary: _secondaryBackground\n        ),\n        thumbnailBackground: _secondaryBackground,\n        contrastingLabel: _primary,\n        accent: _purple,\n        neutralAccent: _secondaryBackground,\n        colorfulAccents: [_orange, _pink, _cyan, _green, _purple, _red, _yellow],\n        commentIndentColors: [_cyan, _green, _orange, _pink, _purple, _red],\n        accountAgeColors: [_green, _cyan],\n        positive: _green,\n        negative: _red,\n        warning: _red,\n        caution: _orange,\n        upvote: _cyan,\n        downvote: _red,\n        save: _green,\n        read: _purple,\n        favorite: _cyan,\n        administration: _purple,\n        moderation: _pink,\n        federatedFeed: _pink,\n        localFeed: _purple,\n        subscribedFeed: _red,\n        moderatedFeed: _pink,\n        savedFeed: _green,\n        popularFeed: _cyan,\n        suggestedFeed: _orange,\n        inbox: _purple,\n        fediseerEndorsement: _cyan\n    )\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/Colors/Palette+Monochrome.swift",
    "content": "//\n//  Palette+Monochrome.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-08.\n//\n\nimport Foundation\nimport SwiftUI\nimport Theming\n\nextension Palette {\n    static let monochrome: Self = .init(\n        bordered: false,\n        label: .init(\n            primary: .primary,\n            secondary: .secondary,\n            tertiary: .init(uiColor: .tertiaryLabel)\n        ),\n        background: .init(\n            primary: .init(uiColor: .systemBackground),\n            secondary: .init(uiColor: .secondarySystemBackground),\n            tertiary: .init(uiColor: .tertiarySystemBackground)\n        ),\n        groupedBackground: .init(\n            primary: .init(uiColor: .systemGroupedBackground),\n            secondary: .init(uiColor: .secondarySystemGroupedBackground),\n            tertiary: .init(uiColor: .tertiarySystemGroupedBackground)\n        ),\n        thumbnailBackground: Color(uiColor: .systemGray4),\n        contrastingLabel: Color(uiColor: .systemBackground),\n        accent: .primary,\n        neutralAccent: .gray,\n        colorfulAccents: [.gray],\n        commentIndentColors: [\n            Color(uiColor: .systemGray),\n            Color(uiColor: .systemGray2),\n            Color(uiColor: .systemGray3),\n            Color(uiColor: .systemGray4),\n            Color(uiColor: .systemGray5),\n            Color(uiColor: .systemGray6)\n        ],\n        accountAgeColors: [.gray],\n        positive: .primary,\n        negative: .primary,\n        warning: .primary,\n        caution: .primary,\n        upvote: .primary,\n        downvote: .primary,\n        save: .primary,\n        read: .primary,\n        favorite: .primary,\n        administration: .primary,\n        moderation: .primary,\n        federatedFeed: Color(uiColor: .darkGray),\n        localFeed: Color(uiColor: .darkGray),\n        subscribedFeed: Color(uiColor: .darkGray),\n        moderatedFeed: Color(uiColor: .darkGray),\n        savedFeed: Color(uiColor: .darkGray),\n        popularFeed: Color(uiColor: .darkGray),\n        suggestedFeed: Color(uiColor: .darkGray),\n        inbox: Color(uiColor: .darkGray),\n        fediseerEndorsement: .gray\n    )\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/Colors/Palette+Oled.swift",
    "content": "//\n//  Palette+Oled.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-08.\n//\n\nimport Foundation\nimport Theming\n\nextension Palette {\n    static let oled: Self = {\n        var palette: Self = .default\n        palette.bordered = true\n        palette.background.primary = .black\n        palette.background.secondary = .black\n        palette.background.tertiary = .black\n        palette.groupedBackground.primary = .black\n        palette.groupedBackground.secondary = .black\n        palette.groupedBackground.tertiary = .black\n        return palette\n    }()\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/Colors/Palette+Solarized.swift",
    "content": "//\n//  Palette+Solarized.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-08.\n//\n\nimport Foundation\nimport SwiftUI\nimport Theming\n\n// See https://ethanschoonover.com/solarized/ for details\n// TODO: I'd love to do this in LAB space, but that involves some ugly manual CGColor work\nprivate let base04: Color = .init(red: 0.0, green: 0.0823529412, blue: 0.1137254902)\nprivate let base03: Color = .init(red: 0.0, green: 0.16862745098039217, blue: 0.21176470588235294)\nprivate let base02: Color = .init(red: 0.027450980392156862, green: 0.21176470588235294, blue: 0.25882352941176473)\nprivate let base01: Color = .init(red: 0.34509803921568627, green: 0.43137254901960786, blue: 0.4588235294117647)\nprivate let base00: Color = .init(red: 0.396078431372549, green: 0.4823529411764706, blue: 0.5137254901960784)\nprivate let base0: Color = .init(red: 0.5137254901960784, green: 0.5803921568627451, blue: 0.5882352941176471)\nprivate let base1: Color = .init(red: 0.5764705882352941, green: 0.6313725490196078, blue: 0.6313725490196078)\nprivate let base2: Color = .init(red: 0.9333333333333333, green: 0.9098039215686274, blue: 0.8352941176470589)\nprivate let base3: Color = .init(red: 0.9921568627450981, green: 0.9647058823529412, blue: 0.8901960784313725)\nprivate let yellow: Color = .init(red: 0.7098039215686275, green: 0.5372549019607843, blue: 0.0)\nprivate let orange: Color = .init(red: 0.796078431372549, green: 0.29411764705882354, blue: 0.08627450980392157)\nprivate let red: Color = .init(red: 0.8627450980392157, green: 0.19607843137254902, blue: 0.1843137254901961)\nprivate let magenta: Color = .init(red: 0.8274509803921568, green: 0.21176470588235294, blue: 0.5098039215686274)\nprivate let violet: Color = .init(red: 0.4235294117647059, green: 0.44313725490196076, blue: 0.7686274509803922)\nprivate let blue: Color = .init(red: 0.14901960784313725, green: 0.5450980392156862, blue: 0.8235294117647058)\nprivate let cyan: Color = .init(red: 0.16470588235294117, green: 0.6313725490196078, blue: 0.596078431372549)\nprivate let green: Color = .init(red: 0.5215686274509804, green: 0.6, blue: 0.0)\n\nextension Palette {\n    static let solarized: Self = .init(\n        bordered: false,\n        label: .init(\n            primary: .init(light: base00, dark: base0),\n            secondary: .init(light: base0, dark: base01),\n            tertiary: .init(light: base1, dark: base01)\n        ),\n        background: .init(\n            primary: .init(light: base3, dark: base03),\n            secondary: .init(light: base2, dark: base02),\n            tertiary: .init(light: base2, dark: base02)\n        ),\n        groupedBackground: .init(\n            primary: .init(light: base2, dark: base04),\n            secondary: .init(light: base3, dark: base03),\n            tertiary: .init(light: base2, dark: base02)\n        ),\n        thumbnailBackground: .init(light: base2, dark: base02),\n        contrastingLabel: base2,\n        accent: blue,\n        neutralAccent: base0,\n        colorfulAccents: [orange, violet, blue, cyan, magenta, green, cyan],\n        commentIndentColors: [red, orange, yellow, cyan, blue, violet],\n        accountAgeColors: [cyan, blue, violet],\n        positive: cyan,\n        negative: red,\n        warning: red,\n        caution: orange,\n        upvote: blue,\n        downvote: red,\n        save: cyan,\n        read: violet,\n        favorite: blue,\n        administration: violet,\n        moderation: green,\n        federatedFeed: blue,\n        localFeed: violet,\n        subscribedFeed: red,\n        moderatedFeed: violet,\n        savedFeed: cyan,\n        popularFeed: magenta,\n        suggestedFeed: orange,\n        inbox: violet,\n        fediseerEndorsement: cyan\n    )\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/Constants/Constants.swift",
    "content": "//\n//  Constants.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-07.\n//\n\nimport Foundation\nimport KeychainAccess\nimport MlemMiddleware\nimport SwiftUI\n\nclass Constants {\n    private var platformConstants: PlatformConstants\n    \n    public static let main: Constants = .init()\n    \n    private init() {\n        if UIDevice.isPhone {\n            self.platformConstants = .phone\n        } else if UIDevice.isPad {\n            self.platformConstants = .pad\n        } else {\n            assertionFailure(\"Unrecognized UIDevice!\")\n            self.platformConstants = .phone\n        }\n    }\n    \n    // MARK: - Common Constants\n    \n    // These constants are used across all platforms, and generally configure backend behavior\n    \n    // MARK: Image Caching\n    \n    /// Size for the image cache (500MB)\n    let cacheSize = 500_000_000\n    /// URLCache to use for image caching\n    let urlCache: URLCache = .init(memoryCapacity: 500_000_000, diskCapacity: 500_000_000)\n    /// URLSession to use for image caching\n    let urlSession: URLSession = .init(configuration: .default)\n    /// Images are fetched at this resolution when displayed in the feed, and the maximum resolution is only fetched when the image viewer is opened\n    let feedImageResolution: Int = 1024\n    \n    // MARK: Keychain\n\n    let keychain: Keychain = .init(service: \"com.hanners.Mlem-keychain\")\n    \n    // MARK: - Platform Constants\n\n    // These constants change depending on which platform the app is running on, and so passthrough to the current PlatformConstants. Standard dimensions are used for elements or element types that recur frequently, and are preferred over non-standard to promote a consistent layout structure. Non-standard spacings are used in cases where unique aesthetic considerations warrant deviation from the standards.\n    \n    // MARK: Standard Spacings\n    \n    /// Normal spacing between elements\n    var standardSpacing: CGFloat { platformConstants.standardSpacing }\n    /// Half of standardSpacing\n    var halfSpacing: CGFloat { platformConstants.halfSpacing }\n    /// Twice standardSpacing\n    var doubleSpacing: CGFloat { platformConstants.doubleSpacing }\n    /// Normal pacing between elements in a compact layout\n    var compactSpacing: CGFloat { platformConstants.compactSpacing }\n    \n    // MARK: Standard Corner Radii\n    \n    /// Corner radius of a large item (tile post)\n    var largeItemCornerRadius: CGFloat { platformConstants.largeItemCornerRadius }\n    /// Corner radius of a medium item (website previews, large cards, etc.)\n    var mediumItemCornerRadius: CGFloat { platformConstants.mediumItemCornerRadius }\n    /// Corner radius of a small item (thumbnails, embedded cards, etc.)\n    var smallItemCornerRadius: CGFloat { platformConstants.smallItemCornerRadius }\n    \n    // MARK: Sizes\n    \n    /// Size of a post thumbnail\n    var thumbnailSize: CGFloat { platformConstants.thumbnailSize }\n    /// Size of an avatar in a list context\n    var listRowAvatarSize: CGFloat { platformConstants.listRowAvatarSize }\n    /// Size of an avatar in a large label display\n    var largeAvatarSize: CGFloat { platformConstants.largeAvatarSize }\n    /// Size of an avatar in a medium label display\n    var mediumAvatarSize: CGFloat { platformConstants.mediumAvatarSize }\n    /// Size of an avatar in a compact label display\n    var smallAvatarSize: CGFloat { platformConstants.smallAvatarSize }\n    /// Size of a feed header avatar\n    var feedHeaderSize: CGFloat { platformConstants.feedHeaderSize }\n    \n    // MARK: Non-Standard Dimensions\n    \n    // App Icon\n    \n    /// Size of an app icon\n    var appIconSize: CGFloat { platformConstants.appIconSize }\n    /// Corner radius of an app icon\n    var appIconCornerRadius: CGFloat { platformConstants.appIconCornerRadius }\n    \n    // Settings Icon\n    \n    /// Size of a settings icon\n    var settingsIconSize: CGFloat { platformConstants.settingsIconSize }\n    \n    // Interaction Bar\n    \n    /// Size of an interaction bar icon\n    var barIconSize: CGFloat { platformConstants.barIconSize }\n    /// Corner radius of an interaction bar icon's background\n    var barIconCornerRadius: CGFloat { platformConstants.barIconCornerRadius }\n    /// Size of the visible bar icon background\n    var barIconBackgroundSize: CGFloat { barIconHitbox - (2 * standardSpacing) }\n    /// Tappable area for a bar icon (extends beyond visible background, should be at least 44x44 per Apple HIG)\n    var barIconHitbox: CGFloat { platformConstants.barIconHitbox }\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/Constants/Platform Constants/PadConstants.swift",
    "content": "//\n//  PadConstants.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-07.\n//\n\nimport Foundation\n\n// iPad-specific constants\nextension PlatformConstants {\n    static let pad: PlatformConstants = .init(\n        standardSpacing: 10,\n        halfSpacing: 5,\n        doubleSpacing: 20,\n        compactSpacing: 6,\n        thumbnailSize: 60,\n        listRowAvatarSize: 46,\n        largeAvatarSize: 32,\n        mediumAvatarSize: 22,\n        smallAvatarSize: 16,\n        feedHeaderSize: 44,\n        largeItemCornerRadius: 16,\n        mediumItemCornerRadius: 8,\n        smallItemCornerRadius: 6,\n        appIconSize: 60,\n        appIconCornerRadius: 10,\n        settingsIconSize: 28,\n        barIconSize: 15.5,\n        barIconCornerRadius: 4,\n        barIconHitbox: 44\n    )\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/Constants/Platform Constants/PhoneConstants.swift",
    "content": "//\n//  PhoneConstants.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-07.\n//\n\nimport Foundation\n\n// iPhone-specific constants\nextension PlatformConstants {\n    static let phone: PlatformConstants = .init(\n        standardSpacing: 10,\n        halfSpacing: 5,\n        doubleSpacing: 20,\n        compactSpacing: 6,\n        thumbnailSize: 60,\n        listRowAvatarSize: 46,\n        largeAvatarSize: 32,\n        mediumAvatarSize: 22,\n        smallAvatarSize: 16,\n        feedHeaderSize: 44,\n        largeItemCornerRadius: 16,\n        mediumItemCornerRadius: 8,\n        smallItemCornerRadius: 6,\n        appIconSize: 60,\n        appIconCornerRadius: 10,\n        settingsIconSize: 28,\n        barIconSize: 15.5,\n        barIconCornerRadius: 4,\n        barIconHitbox: 44\n    )\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/Constants/Platform Constants/PlatformConstants.swift",
    "content": "//\n//  PlatformConstants.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-07.\n//\n\nimport Foundation\n\n// Struct enumerating all platform-specific constants\nstruct PlatformConstants {\n    // Standard spacings\n    let standardSpacing: CGFloat\n    let halfSpacing: CGFloat\n    let doubleSpacing: CGFloat\n    let compactSpacing: CGFloat\n    \n    // Standard sizes\n    let thumbnailSize: CGFloat\n    let listRowAvatarSize: CGFloat\n    let largeAvatarSize: CGFloat\n    let mediumAvatarSize: CGFloat\n    let smallAvatarSize: CGFloat\n    let feedHeaderSize: CGFloat\n    \n    // Standard corner radii\n    let largeItemCornerRadius: CGFloat\n    let mediumItemCornerRadius: CGFloat\n    let smallItemCornerRadius: CGFloat\n    \n    // Non-standard dimensions\n    let appIconSize: CGFloat\n    let appIconCornerRadius: CGFloat\n    let settingsIconSize: CGFloat\n    let barIconSize: CGFloat\n    let barIconCornerRadius: CGFloat\n    let barIconHitbox: CGFloat\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/Icons.swift",
    "content": "//\n//  Icon.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-09-13.\n//\n\nimport Foundation\nimport SwiftUI\n\n// swiftlint:disable type_body_length\n\n/// SFSymbol names for icons\nenum Icons {\n    // votes\n    static let votes: String = \"arrow.up.arrow.down\"\n    static let votesSquare: String = \"arrow.up.arrow.down.square\"\n    static let upvote: String = \"arrow.up\"\n    static let upvoteSquare: String = \"arrow.up.square\"\n    static let upvoteSquareFill: String = \"arrow.up.square.fill\"\n    static let downvote: String = \"arrow.down\"\n    static let downvoteSquare: String = \"arrow.down.square\"\n    static let downvoteSquareFill: String = \"arrow.down.square.fill\"\n    static let resetVoteSquare: String = \"minus.square\"\n    static let resetVoteSquareFill: String = \"minus.square.fill\"\n    \n    // reply/send\n    static let reply: String = \"arrowshape.turn.up.left\"\n    static let replyFill: String = \"arrowshape.turn.up.left.fill\"\n    static let send: String = \"paperplane\"\n    static let sendFill: String = \"paperplane.fill\"\n    static let sendMessage: String = \"arrow.up.circle.fill\"\n    \n    // save\n    static let save: String = \"bookmark\"\n    static let saveFill: String = \"bookmark.fill\"\n    static let unsave: String = \"bookmark.slash\"\n    static let unsaveFill: String = \"bookmark.slash.fill\"\n    \n    // mark read\n    static let markRead: String = \"envelope.open\"\n    static let markReadFill: String = \"envelope.open.fill\"\n    static let markUnread: String = \"envelope\"\n    static let markUnreadFill: String = \"envelope.fill\"\n    \n    // moderation\n    static let moderation: String = \"shield\"\n    static let moderationFill: String = \"shield.fill\"\n    static let administration: String = \"crown\"\n    static let administrationFill: String = \"crown.fill\"\n    static let demoteModerator: String = \"shield.slash\"\n    static let demoteModeratorFill: String = \"shield.slash.fill\"\n    static let moderationReport: String = \"flag\"\n    static let moderationReportFill: String = \"flag.fill\"\n    static let registrationApplication: String = \"list.clipboard\"\n    static let modlog: String = \"book.pages\"\n    static let transferCommunity: String = \"arrow.right\"\n    static let removeAdministrator: String = \"arrowshape.down\"\n    static let removeAdministratorFill: String = \"arrowshape.down.fill\"\n    static let resolve: String = \"checkmark.circle\"\n    static let resolveFill: String = \"checkmark.circle.fill\"\n    static let unresolve: String = \"xmark.circle\"\n    static let unresolveFill: String = \"xmark.circle.fill\"\n    \n    // inbox\n    static let mention: String = \"quote.bubble\"\n    static let message: String = \"envelope\"\n    \n    // misc post\n    static let posts: String = \"doc.plaintext\"\n    static let replies: String = \"bubble.left\"\n    static let unreadReplies: String = \"text.bubble\"\n    static let textPost: String = \"text.book.closed\"\n    static let titleOnlyPost: String = \"character.bubble\"\n    static let pin: String = \"pin\"\n    static let pinFill: String = \"pin.fill\"\n    static let unpin: String = \"pin.slash\"\n    static let unpinFill: String = \"pin.slash.fill\"\n    static let websiteIcon: String = \"globe\"\n    static let read: String = \"book\"\n    static let lock: String = \"lock\"\n    static let lockFill: String = \"lock.fill\"\n    static let unlock: String = \"lock.open\"\n    static let unlockFill: String = \"lock.open.fill\"\n    static let remove: String = \"xmark.bin\"\n    static let removeFill: String = \"xmark.bin.fill\"\n    static let restore: String = \"arrow.up.bin\"\n    static let restoreFill: String = \"arrow.up.bin.fill\"\n    static let purge: String = \"burn\"\n    static let scoreCounter: String = \"arrow.up.arrow.down.circle\"\n    static let upvoteCounter: String = \"arrow.up.circle\"\n    static let downvoteCounter: String = \"arrow.down.circle\"\n    static let replyCounter: String = \"arrowshape.turn.up.left.circle\"\n    \n    // post sizes\n    static let postSizeSetting: String = \"rectangle.expand.vertical\"\n    static let compactPost: String = \"rectangle.grid.1x2\"\n    static let compactPostFill: String = \"rectangle.grid.1x2.fill\"\n    static let tilePost: String = \"square.grid.2x2\"\n    static let tilePostFill: String = \"square.grid.2x2.fill\"\n    static let headlinePost: String = \"rectangle\"\n    static let headlinePostFill: String = \"rectangle.fill\"\n    static let largePost: String = \"text.below.photo\"\n    static let largePostFill: String = \"text.below.photo.fill\"\n    \n    // feeds\n    static let federatedFeed: String = \"circle.hexagongrid\"\n    static let federatedFeedFill: String = \"circle.hexagongrid.fill\"\n    static let federatedFeedCircle: String = \"circle.hexagongrid.circle.fill\"\n    static let instanceFeed: String = \"building.2\"\n    static let instanceFeedFill: String = \"building.2.fill\"\n    static let instanceFeedCircle: String = \"building.2.crop.circle\"\n    static let subscribedFeed: String = \"newspaper\"\n    static let subscribedFeedFill: String = \"newspaper.fill\"\n    static let subscribedFeedCircle: String = \"newspaper.circle.fill\"\n    static let savedFeed: String = \"bookmark\"\n    static let savedFeedFill: String = \"bookmark.fill\"\n    static let savedFeedCircle: String = \"bookmark.circle.fill\"\n    \n    // sort types\n    static let activeSort: String = \"popcorn\"\n    static let activeSortFill: String = \"popcorn.fill\"\n    static let hotSort: String = \"flame\"\n    static let hotSortFill: String = \"flame.fill\"\n    static let scaledSort: String = \"arrow.up.left.and.down.right.and.arrow.up.right.and.down.left\"\n    static let scaledSortFill: String = \"arrow.up.left.and.down.right.and.arrow.up.right.and.down.left\"\n    static let newSort: String = \"hare\"\n    static let newSortFill: String = \"hare.fill\"\n    static let oldSort: String = \"tortoise\"\n    static let oldSortFill: String = \"tortoise.fill\"\n    static let newCommentsSort: String = \"exclamationmark.bubble\"\n    static let newCommentsSortFill: String = \"exclamationmark.bubble.fill\"\n    static let mostCommentsSort: String = \"bubble.left.and.bubble.right\"\n    static let mostCommentsSortFill: String = \"bubble.left.and.bubble.right.fill\"\n    static let controversialSort: String = \"bolt\"\n    static let controversialSortFill: String = \"bolt.fill\"\n    static let topSortMenu: String = \"text.line.first.and.arrowtriangle.forward\"\n    static let topSort: String = \"trophy\"\n    static let topSortFill: String = \"trophy.fill\"\n    static let timeSort: String = \"calendar.day.timeline.leading\"\n    static let timeSortFill: String = \"calendar.day.timeline.leading\"\n    static let alphabeticalSort: String = \"textformat\"\n    static let scoreSort: String = \"star\"\n    static let usersSort: String = \"person.2\"\n    static let versionSort: String = \"server.rack\"\n    \n    // user flairs\n    static let developerFlair: String = \"hammer.fill\"\n    static let botFlair: String = \"terminal.fill\"\n    static let opFlair: String = \"person.fill\"\n    static let instanceBannedFlair: String = \"xmark.square.fill\"\n    static let communityBannedFlair: String = \"xmark.shield.fill\"\n    static let newAccountFlair: String = \"leaf.fill\"\n    \n    // markdown\n    static let bold: String = \"bold\"\n    static let italic: String = \"italic\"\n    static let strikethrough: String = \"strikethrough\"\n    static let superscript: String = \"textformat.superscript\"\n    static let `subscript`: String = \"textformat.subscript\"\n    // Potentially \"chevron.left.chevron.right\" is better, it's iOS 18+ though\n    static let inlineCode: String = \"chevron.left.forwardslash.chevron.right\"\n    static let quote: String = \"quote.opening\"\n    static let heading: String = \"textformat.size\"\n    static let uploadImage: String = \"photo\"\n    static let spoiler: String = \"eye\"\n    static let codeBlock: String = \"text.viewfinder\"\n    \n    // entities/general Lemmy concepts\n    static let federation: String = \"point.3.filled.connected.trianglepath.dotted\"\n    static let instance: String = \"building.2\"\n    static let instanceFill: String = \"building.2.fill\"\n    static let instanceCircle: String = \"building.2.crop.circle\"\n    static let instanceCircleFill: String = \"building.2.crop.circle.fill\"\n    static let person: String = \"person\"\n    static let personFill: String = \"person.fill\"\n    static let personCircle: String = \"person.crop.circle\"\n    static let personCircleFill: String = \"person.crop.circle.fill\"\n    static let community: String = \"house\"\n    static let communityFill: String = \"house.fill\"\n    static let communityCircle: String = \"house.circle\"\n    static let communityCircleFill: String = \"house.circle.fill\"\n    \n    // tabs\n    static let feeds: String = \"scroll\"\n    static let feedsFill: String = \"scroll.fill\"\n    static let inbox: String = \"mail.stack\"\n    static let inboxFill: String = \"mail.stack.fill\"\n    static let search: String = \"magnifyingglass\"\n    static let searchActive: String = \"text.magnifyingglass\"\n    static let settings: String = \"gear\"\n    \n    // information/status\n    static let success: String = \"checkmark\"\n    static let successCircle: String = \"checkmark.circle\"\n    static let successCircleFill: String = \"checkmark.circle.fill\"\n    static let successSquareFill: String = \"checkmark.square.fill\"\n    static let failure: String = \"xmark\"\n    static let failureCircle: String = \"xmark.circle\"\n    static let failureCircleFill: String = \"xmark.circle.fill\"\n    static let present: String = \"circle.fill\" // that's present as in \"here,\" not as in \"gift\"\n    static let absent: String = \"circle\"\n    static let warning: String = \"exclamationmark.triangle\"\n    static let warningFill: String = \"exclamationmark.triangle.fill\"\n    static let hide: String = \"eye.slash\"\n    static let hideFill: String = \"eye.slash.fill\"\n    static let block: String = \"hand.raised\"\n    static let blockFill: String = Icons.privacy\n    static let unblock: String = \"hand.raised.slash\"\n    static let unblockFill: String = \"hand.raised.slash.fill\"\n    static let nsfwTag: String = \"nsfw\"\n    static let show: String = \"eye\"\n    static let showFill: String = \"eye.fill\"\n    static let blurNsfw: String = \"eye.trianglebadge.exclamationmark\"\n    static let noContent: String = \"binoculars\"\n    static let noPosts: String = \"text.bubble\"\n    static let time: String = \"clock\"\n    static let updated: String = \"clock.arrow.2.circlepath\"\n    static let favorite: String = \"star\"\n    static let favoriteFill: String = \"star.fill\"\n    static let unfavorite: String = \"star.slash\"\n    static let unfavoriteFill: String = \"star.slash.fill\"\n    static let close: String = \"multiply\"\n    static let closeCircle: String = \"xmark.circle\"\n    static let closeCircleFill: String = \"xmark.circle.fill\"\n    static let addCircleFill: String = \"plus.circle.fill\"\n    static let cakeDay: String = \"birthday.cake\"\n    static let cakeDayFill: String = \"birthday.cake.fill\"\n    static let undoCircleFill: String = \"arrow.uturn.backward.circle.fill\"\n    static let errorCircleFill: String = \"exclamationmark.circle.fill\"\n    static let proxy: String = \"firewall\"\n    \n    // uptime\n    static let uptimeOffline: String = \"xmark.circle.fill\"\n    static let uptimeOnline: String = \"checkmark.circle.fill\"\n    static let uptimeOutage: String = \"exclamationmark.circle.fill\"\n    \n    // end of feed\n    static let endOfFeedHobbit: String = \"figure.climbing\"\n    static let endOfFeedCartoon: String = \"figure.wave\"\n    static let endOfFeedTurtle: String = \"tortoise\"\n    \n    // common operations\n    static let share: String = \"square.and.arrow.up\"\n    static let subscribe: String = \"plus.circle\"\n    static let subscribed: String = \"checkmark.circle\"\n    static let subscribePerson: String = \"person.crop.circle.badge.plus\"\n    static let subscribePersonFill: String = \"person.crop.circle.badge.plus.fill\"\n    static let unsubscribe: String = \"multiply.circle\"\n    static let unsubscribePerson: String = \"person.crop.circle.badge.xmark\"\n    static let unsubscribePersonFill: String = \"person.crop.circle.badge.xmark.fill\"\n    static let filter: String = \"line.3.horizontal.decrease.circle\"\n    static let filterFill: String = \"line.3.horizontal.decrease.circle.fill\"\n    static let menu: String = \"ellipsis\"\n    static let menuCircle: String = \"ellipsis.circle\"\n    static let menuCircleFill: String = \"ellipsis.circle.fill\"\n    static let `import`: String = \"square.and.arrow.down\"\n    static let attachment: String = \"paperclip\"\n    static let edit: String = \"pencil\"\n    static let delete: String = \"trash\"\n    static let deleteFill: String = \"trash.fill\"\n    static let undelete: String = \"arrow.up.trash\"\n    static let copy: String = \"doc.on.doc\"\n    static let copyFill: String = \"doc.on.doc.fill\"\n    static let paste: String = \"doc.on.clipboard\"\n    static let signOut: String = \"minus.circle\"\n    static let collapseComment: String = \"arrow.down.and.line.horizontal.and.arrow.up\"\n    static let expandComment: String = \"arrow.up.and.line.horizontal.and.arrow.down\"\n    static let refresh: String = \"arrow.clockwise\"\n    static let select: String = \"selection.pin.in.out\"\n    static let crossPost: String = \"shuffle\"\n    static let chooseFile: String = \"folder\"\n    static let add: String = \"plus\"\n    static let createImage: String = \"scanner\"\n    \n    // collapse actions\n    static let collapse: String = \"minus\"\n    static let collapseSquare: String = \"minus.rectangle\"\n    static let collapseSquareFill: String = \"minus.rectangle.fill\"\n    static let collapseParent: String = \"chevron.up\"\n    static let collapseParentSquare: String = \"chevron.up.square\"\n    static let collapseParentSquareFill: String = \"chevron.up.square.fill\"\n    static let collapseToTop: String = \"arrow.up.to.line\"\n    static let collapseToTopSquare: String = \"arrow.up.to.line.square\"\n    static let collapseToTopSquareFill: String = \"arrow.up.to.line.square.fill\"\n    \n    // settings\n    static let upvoteOnSave: String = \"arrow.up.heart\"\n    static let readIndicatorSetting: String = \"book\"\n    static let readIndicatorBarSetting: String = \"rectangle.leftthird.inset.filled\"\n    static let profileTabSettings: String = \"person.text.rectangle\"\n    static let nicknameField: String = \"rectangle.and.pencil.and.ellipsis\"\n    static let label: String = \"tag\"\n    static let unreadBadge: String = \"envelope.badge\"\n    static let showAvatar: String = \"person.fill.questionmark\"\n    static let widgetWizard: String = \"wand.and.stars\"\n    static let thumbnail: String = \"photo\"\n    static let author: String = \"signature\"\n    static let websiteAddress: String = \"link\"\n    static let leftRight: String = \"arrow.left.arrow.right\"\n    static let leftAndRightCircle: String = \"arrow.left.and.right.circle\"\n    static let developerMode: String = \"wrench.adjustable.fill\"\n    static let limitImageHeightSetting: String = \"rectangle.compress.vertical\"\n    static let appLockSettings: String = \"lock.app.dashed\"\n    static let banFromInstance: String = \"xmark.square\"\n    static let unbanFromInstance: String = \"checkmark.square\"\n    static let banFromCommunity: String = \"xmark.shield\"\n    static let unbanFromCommunity: String = \"checkmark.shield\"\n    static let banFromInstanceFill: String = \"xmark.square.fill\"\n    static let unbanFromInstanceFill: String = \"checkmark.square.fill\"\n    static let banFromCommunityFill: String = \"xmark.shield.fill\"\n    static let unbanFromCommunityFill: String = \"checkmark.shield.fill\"\n    static let logIn: String = \"person.text.rectangle\"\n    static let signUp: String = \"pencil.and.list.clipboard\"\n    static let sidebar: String = \"sidebar.left\"\n    static let infiniteScroll: String = \"infinity\"\n    static let confirmImageUploads: String = \"photo.badge.checkmark\"\n    static let swipeActions: String = \"inset.filled.leadinghalf.rectangle\"\n    static let swipeAnywhere: String = \"arrow.left\"\n    static let importSettings: String = \"folder.badge.gearshape\"\n    static let inApp: String = \"house\"\n    static let reader: String = \"text.page\"\n    static let keywordFilter: String = \"rectangle.and.text.magnifyingglass\"\n    static let saveSettings: String = \"document.badge.gearshape\"\n    static let restoreSettings: String = \"gearshape.arrow.trianglehead.2.clockwise.rotate.90\"\n    static let menuItems: String = \"filemenu.and.selection\"\n    static let systemMode: String = \"circle.lefthalf.filled\"\n    static let lightMode: String = \"sun.max\"\n    static let darkMode: String = \"moon\"\n    static let compactComments: String = \"rectangle.compress.vertical\"\n    static let interactionBar: String = \"square.and.line.vertical.and.square.fill\"\n    static let commentDepth: String = \"text.append\"\n    static let qualifiedLabel: String = \"at\"\n    static let right: String = \"arrow.right.circle\"\n    static let left: String = \"arrow.left.circle\"\n    static let center: String = \"dot.circle\"\n    static let zoomSlider: String = \"arrow.up.and.down.and.sparkles\"\n    static let language: String = \"globe\"\n    \n    // fediseer\n    static let fediseer: String = \"shield.checkered\"\n    static let fediseerGuarantee: String = \"checkmark.seal.fill\"\n    static let fediseerUnguarantee: String = \"xmark.seal.fill\"\n    static let fediseerEndorsement: String = \"signature\"\n    static let fediseerHesitation: String = \"exclamationmark.triangle.fill\"\n    static let fediseerCensure: String = \"exclamationmark.octagon.fill\"\n    \n    // media\n    static let play: String = \"play.fill\"\n    static let playCircle: String = \"play.circle\"\n    static let pause: String = \"pause.fill\"\n    static let muted: String = \"speaker.slash.fill\"\n    static let unmuted: String = \"speaker.wave.2.fill\"\n    static let embedding: String = \"app.connected.to.app.below.fill\"\n    static let movie: String = \"film\"\n    \n    // misc\n    static let `private`: String = \"lock\"\n    static let email: String = \"envelope\"\n    static let photo: String = \"photo\"\n    static let action: String = \"diamond\"\n    static let switchUser: String = \"person.crop.circle.badge.plus\"\n    static let missing: String = \"questionmark.square.dashed\"\n    static let connection: String = \"antenna.radiowaves.left.and.right\"\n    static let haptics: String = \"circle.dotted.and.circle\"\n    static let transparency: String = \"square.on.square.intersection.dashed\"\n    static let icon: String = \"fleuron\"\n    static let banner: String = \"flag\"\n    static let noWifi: String = \"wifi.slash\"\n    static let easterEgg: String = \"gift.fill\"\n    static let jumpButton: String = \"chevron.down\"\n    static let jumpButtonCircle: String = \"chevron.down.circle\"\n    static let jumpToLastPositionButton: String = \"chevron.down.2\"\n    static let browser: String = \"safari\"\n    static let emptySquare: String = \"square\"\n    static let dropDown: String = \"chevron.down\"\n    static let dropDownCircleFill: String = \"chevron.down.circle.fill\"\n    static let noFile: String = \"questionmark.folder\"\n    static let forward: String = \"chevron.forward\"\n    static let backward: String = \"chevron.backward\"\n    static let imageDetails: String = \"doc.badge.ellipsis\"\n    static let accountSwitchReload: String = \"arrow.2.circlepath\"\n    static let accountSwitchKeepPlace: String = \"checkmark.diamond\"\n    static let security: String = \"key\"\n    static let securityFill: String = \"key.fill\"\n    static let privacy: String = \"hand.raised.fill\"\n}\n\n// swiftlint:enable type_body_length\n"
  },
  {
    "path": "Mlem/App/Configuration/User Settings/PinnedSortTracker.swift",
    "content": "//\n//  PinnedSortTracker.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-12.\n//\n\nimport Dependencies\nimport Foundation\nimport MlemMiddleware\nimport Observation\n\n@Observable\nclass PinnedSortTracker {\n    @ObservationIgnored @Dependency(\\.persistenceRepository)\n    private var persistenceRepository\n    \n    var pinnedSortTypes: Set<PostSortType> {\n        didSet { Task.detached {\n            try await self.persistenceRepository.savePinnedSortTypes(self.pinnedSortTypes)\n        } }\n    }\n    \n    init() {\n        self.pinnedSortTypes = PersistenceRepository.liveValue.loadPinnedSortTypes()\n    }\n    \n    public static let main: PinnedSortTracker = .init()\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/User Settings/SettingPropertyWrapper.swift",
    "content": "//\n//  SettingPropertyWrapper.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-07.\n//  Adapted from https://fatbobman.com/en/posts/appstorage/\n//\n\nimport Foundation\nimport SwiftUI\n\n@propertyWrapper\nstruct Setting<T>: DynamicProperty {\n    private let keyPath: ReferenceWritableKeyPath<SettingsValues, T>\n    \n    public init(_ keyPath: ReferenceWritableKeyPath<SettingsValues, T>) {\n        self.keyPath = keyPath\n    }\n    \n    public var wrappedValue: T {\n        get { Settings.get(keyPath) }\n        nonmutating set { Settings.set(keyPath, to: newValue) }\n    }\n    \n    public var projectedValue: Binding<T> {\n        Binding(\n            get: { Settings.get(keyPath) },\n            set: { Settings.set(keyPath, to: $0) }\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/User Settings/Settings.swift",
    "content": "//\n//  CodableSettings.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-09-05.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport UIKit\nimport Dependencies\nimport SwiftUI\n\n/// Responsible for managing settings logic.\n///\n/// There should only ever be one instance of this class, the private `main`. To enforce this, interaction with the class\n/// is entirely abstracted to behind a static API.\n///\n/// To access a settings value, it is recommended to use the `@Setting` property wrapper. In contexts where this is not available,\n/// use `Settings.get(\\.keypath)`.\nclass Settings {\n    @Dependency(\\.persistenceRepository) var persistenceRepository\n    \n    private let values: SettingsValues\n    private static let main: Settings = .init()\n    \n    // MARK: - API\n    \n    static func get<T>(_ keyPath: ReferenceWritableKeyPath<SettingsValues, T>) -> T {\n        main.values[keyPath: keyPath]\n    }\n    \n    static func set<T>(_ keyPath: ReferenceWritableKeyPath<SettingsValues, T>, to newValue: T) {\n        main.values[keyPath: keyPath] = newValue\n        main._save()\n    }\n\n    static func mutate<T>(_ keyPath: ReferenceWritableKeyPath<SettingsValues, T>, mutation: (T) -> T) {\n        main.values[keyPath: keyPath] = mutation(main.values[keyPath: keyPath])\n        main._save()\n    }\n\n    static func mutate<T>(_ keyPath: ReferenceWritableKeyPath<SettingsValues, T>, mutation: (inout T) -> Void) {\n        mutation(&main.values[keyPath: keyPath])\n        main._save()\n    }\n    \n    static func save(to systemSetting: SystemSetting) async {\n        await main._save(to: systemSetting)\n    }\n    \n    @MainActor\n    static func restore(from systemSetting: SystemSetting) {\n        main._restore(from: systemSetting)\n    }\n    \n    @MainActor\n    static func reinit(with values: SettingsValues) {\n        main._reinit(with: values)\n    }\n    \n    static func encoded() throws -> Data {\n        try JSONEncoder().encode(main.values)\n    }\n    \n    // MARK: - Logic\n    \n    fileprivate func _save() {\n        Task {\n            try await persistenceRepository.saveSystemSettings(values, setting: .v2_system)\n        }\n    }\n    \n    private func _save(to systemSetting: SystemSetting) async {\n        do {\n            try await persistenceRepository.saveSystemSettings(values, setting: systemSetting)\n            ToastModel.main.add(.success(\"Saved Settings\"))\n        } catch {\n            handleError(error)\n        }\n    }\n    \n    @MainActor\n    private func _restore(from systemSetting: SystemSetting) {\n        if let savedSettings = persistenceRepository.loadSystemSettings(systemSetting) {\n            _reinit(with: savedSettings)\n            ToastModel.main.add(.success(\"Restored Settings\"))\n        } else {\n            ToastModel.main.add(.failure(\"Could not find settings\"))\n        }\n    }\n    \n    @MainActor\n    private func _reinit(with newValues: SettingsValues) {\n        // values needs to be re-initialized memberwise rather than simply reassigned in order for the changes to publish correctly\n        values.reinit(from: newValues)\n        _save()\n    }\n    \n    private init() {\n        @Dependency(\\.persistenceRepository) var persistenceRepository\n        if let savedSettings = persistenceRepository.loadSystemSettings(.v2_system) {\n            values = savedSettings\n        } else {\n            values = .init(from: .main, filteredKeywords: persistenceRepository.loadFilteredKeywords())\n            Task {\n                do {\n                    try await persistenceRepository.saveSystemSettings(values, setting: .v2_system)\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n    }\n}\n\n// MARK: Legacy Keyword Loading\n\nprivate extension PersistencePath {\n    static var filteredKeywords = root.appendingPathComponent(\"Blocked Keywords\", conformingTo: .json)\n}\n\nprivate extension PersistenceRepository {\n    func loadFilteredKeywords() -> Set<String> {\n        load(Set<String>.self, from: PersistencePath.filteredKeywords) ?? .init()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Configuration/User Settings/SettingsValues.swift",
    "content": "//\n//  SettingsValues.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-04-07.\n//\n\nimport Dependencies\nimport Haptics\nimport MlemMiddleware\nimport UIKit\n\n// swiftlint:disable line_length function_body_length file_length\n\n/// Values backing the Settings class.\n/// - Note: when adding a new settings, be sure to add relevant entries to `init`, `reinit`, and `CodingKeys`.\n@Observable\nclass SettingsValues: Codable { // swiftlint:disable:this type_body_length\n    var a11y_readPostIndicator: ReadPostIndicator\n    var a11y_readOutlineThickness: Int\n    var a11y_showSettingsIcons: Bool\n    var a11y_websiteThumbnailIcon: Bool\n    var a11y_zoomSliderLocation: ZoomSliderLocation\n    var a11y_showInteractionBarButtonBackground: Bool\n    var accounts_defaultId: Int?\n    var accounts_grouped: Bool\n    var accounts_sort: AccountSortMode\n    var accounts_keepPlace: Bool\n    var accounts_preferredListRowComplication: PreferredAccountListRowComplication\n    var appearance_interfaceStyle: UIUserInterfaceStyle\n    var appearance_palette: PaletteOption\n    var markdown_wrapCodeBlockLines: Bool\n    var behavior_biometricUnlock: Bool\n    var behavior_confirmImageUploads: Bool\n    var behavior_enableQuickSwipes: Bool\n    var behavior_hapticLevel: HapticTier?\n    var behavior_internetSpeed: InternetSpeed\n    var behavior_upvoteOnSave: Bool\n    var behavior_autoplayMedia: Bool\n    var behavior_muteVideos: Bool\n    var behavior_infiniteScroll: Bool\n    var comment_behaviors_collapseChildren: Bool\n    var comment_compact: Bool\n    var comment_defaultSort: LemmyCommentSortType\n    var comment_gestures_tapToCollapse: Bool\n    var comment_jumpButton: CommentJumpButtonLocation\n    var comment_showCreatorInstance: Bool\n    var comment_maxDepth: Int\n    var comment_createImage_showPost: Bool\n    var comment_createImage_showCreator: Bool\n    var comment_createImage_showStats: Bool\n    var comment_createImage_colorScheme: UIUserInterfaceStyle\n    var comment_showDownvotesCompact: Bool\n    var community_showAvatar: Bool\n    var community_showBanner: Bool\n    var community_showInstance: Bool\n    var dev_developerMode: Bool\n    var dev_errorTimeout: Double\n    var feed_default: ListingType\n    var feed_markReadOnScroll: Bool\n    var feed_showRead: Bool\n    var inbox_showRead: Bool\n    var links_displayMode: TappableLinksDisplayMode\n    var links_openInBrowser: Bool\n    var links_readerMode: Bool\n    var links_shareMode: LinkSharingMode\n    var links_embedLoops: Bool\n    var imageViewer_showControls: ShowImageViewerControls\n    var imageViewer_showCloseButton: Bool\n    var imageViewer_showZoomIndicator: Bool\n    var imageViewer_dismissThreshold: Int\n    var media_animatedAvatars: AnimatedAvatarBehavior\n    var menus_allModActions: Bool\n    var menus_modActionGrouping: ModeratorActionGrouping\n    var post_defaultSort: LemmySortType\n    var post_fallbackSort: LemmySortType\n    var post_limitImageHeight: Bool\n    var post_showCreator: Bool\n    var post_showCreatorInstance: Bool\n    var post_showSubscribedStatus: Bool\n    var post_showWebsitePreview: Bool\n    var post_size: PostSize\n    var post_allowMultipleColumns: Bool\n    var post_thumbnailLocation: ThumbnailLocation\n    var post_webPreview_showHost: Bool\n    var post_webPreview_showIcon: Bool\n    var post_showDownvotesCompact: Bool\n    var post_gestures_tapToCollapse: Bool\n    var post_createImage_showCommunity: Bool\n    var post_createImage_showCreator: Bool\n    var post_createImage_showStats: Bool\n    var post_createImage_colorScheme: UIUserInterfaceStyle\n    var profile_showBanner: Bool\n    var privacy_autoBypassImageProxy: Bool\n    var privacy_showFavicons: Bool\n    var safety_blurNsfw: NsfwBlurBehavior\n    var safety_enableModlogWarning: Bool\n    var safety_enableNsfwCommunityWarning: Bool\n    var tab_gestures_enableLongPress: Bool\n    var tab_gestures_enableSwipeUp: Bool\n    var tab_gestures_longPressAction: TabBarLongPressAction\n    var tab_profile_labelType: ProfileTabLabel\n    var tab_profile_showAvatar: Bool\n    var tab_inbox_badgeIncludedTypes: Set<InboxItemType>\n    var tab_showNames: Bool\n    var tip_feedWelcomePrompt: Bool\n    var person_showAvatar: Bool\n    var person_showInstance: Bool\n    var person_ageVisibility: AccountAgeFlairVisibility\n    var status_bypassImageProxyShown: Bool\n    var subscriptions_instanceLocation: InstanceLocation\n    var subscriptions_sort: SubscriptionListSort\n    var navigation_sidebarVisibleByDefault: Bool\n    var navigation_swipeAnywhere: Bool\n    var filters_keywordFilterEnabled: Bool\n    var filters_keywords: Set<String>\n    var filters_literalFilterEnabled: Bool\n    var filters_literals: Set<String>\n    \n    var interactionBar_post: PostBarConfiguration\n    var interactionBar_comment: CommentBarConfiguration\n    var interactionBar_reply: ReplyBarConfiguration\n    var interactionBar_community: CommunityActionConfiguration\n    var interactionBar_postReport: PostBarConfiguration\n    var interactionBar_commentReport: CommentBarConfiguration\n    var interactionBar_alternateReportLayout: Bool\n\n    var events_showEvents: Bool\n    \n    // These are included in the encoding, but are synthesized into tab_inbox_badgeIncludedTypes at decoding\n    @ObservationIgnored var inbox_badge_includeApplications: Bool = false\n    @ObservationIgnored var inbox_badge_includeMessageReports: Bool = false\n    @ObservationIgnored var inbox_badge_includeMod: Bool = false\n    @ObservationIgnored var inbox_badge_includePersonal: Bool = false\n    \n    // This was only used in a 2.5 beta; remove me\n    var imageViewer_showOverlayByDefault: Bool = true\n\n    required init(from decoder: any Decoder) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        self.a11y_readPostIndicator = try container.decodeIfPresent(ReadPostIndicator.self, forKey: ._a11y_readPostIndicator) ?? .checkmark\n        self.a11y_readOutlineThickness = try container.decodeIfPresent(Int.self, forKey: ._a11y_readOutlineThickness) ?? 3\n        self.a11y_showSettingsIcons = try container.decodeIfPresent(Bool.self, forKey: ._a11y_showSettingsIcons) ?? true\n        self.a11y_websiteThumbnailIcon = try container.decodeIfPresent(Bool.self, forKey: ._a11y_websiteThumbnailIcon) ?? false\n        self.a11y_zoomSliderLocation = try container.decodeIfPresent(ZoomSliderLocation.self, forKey: ._a11y_zoomSliderLocation) ?? .none\n        self.a11y_showInteractionBarButtonBackground = try container.decodeIfPresent(Bool.self, forKey: ._a11y_showInteractionBarButtonBackground) ?? false\n        self.accounts_defaultId = try container.decodeIfPresent(Int?.self, forKey: ._accounts_defaultId) ?? nil\n        self.accounts_grouped = try container.decodeIfPresent(Bool.self, forKey: ._accounts_grouped) ?? false\n        self.accounts_sort = try container.decodeIfPresent(AccountSortMode.self, forKey: ._accounts_sort) ?? .name\n        self.accounts_keepPlace = try container.decodeIfPresent(Bool.self, forKey: ._accounts_keepPlace) ?? false\n        self.accounts_preferredListRowComplication = try container.decodeIfPresent(PreferredAccountListRowComplication.self, forKey: ._accounts_preferredListRowComplication) ?? .lastUsed\n        self.appearance_interfaceStyle = try container.decodeIfPresent(UIUserInterfaceStyle.self, forKey: ._appearance_interfaceStyle) ?? .unspecified\n        self.appearance_palette = try container.decodeIfPresent(PaletteOption.self, forKey: ._appearance_palette) ?? .standard\n        self.markdown_wrapCodeBlockLines = try container.decodeIfPresent(Bool.self, forKey: ._markdown_wrapCodeBlockLines) ?? true\n        self.behavior_biometricUnlock = try container.decodeIfPresent(Bool.self, forKey: ._behavior_biometricUnlock) ?? false\n        self.behavior_confirmImageUploads = try container.decodeIfPresent(Bool.self, forKey: ._behavior_confirmImageUploads) ?? true\n        self.behavior_enableQuickSwipes = try container.decodeIfPresent(Bool.self, forKey: ._behavior_enableQuickSwipes) ?? true\n        \n        do {\n            self.behavior_hapticLevel = try container.decodeIfPresent(HapticTier.self, forKey: ._behavior_hapticLevel)\n        } catch DecodingError.dataCorrupted { // Decodes the 'sentinel' value, which was replaced with `nil` in Mlem 2.2\n            self.behavior_hapticLevel = nil\n        }\n        \n        self.behavior_internetSpeed = try container.decodeIfPresent(InternetSpeed.self, forKey: ._behavior_internetSpeed) ?? .fast\n        self.behavior_autoplayMedia = try container.decodeIfPresent(Bool.self, forKey: ._behavior_autoplayMedia) ?? false\n        self.behavior_muteVideos = try container.decodeIfPresent(Bool.self, forKey: ._behavior_muteVideos) ?? true\n        self.behavior_upvoteOnSave = try container.decodeIfPresent(Bool.self, forKey: ._behavior_upvoteOnSave) ?? false\n        self.behavior_infiniteScroll = try container.decodeIfPresent(Bool.self, forKey: ._behavior_infiniteScroll) ?? true\n        self.comment_behaviors_collapseChildren = try container.decodeIfPresent(Bool.self, forKey: ._comment_behaviors_collapseChildren) ?? false\n        self.comment_compact = try container.decodeIfPresent(Bool.self, forKey: ._comment_compact) ?? false\n        self.comment_defaultSort = try container.decodeIfPresent(LemmyCommentSortType.self, forKey: ._comment_defaultSort) ?? .hot\n        self.comment_gestures_tapToCollapse = try container.decodeIfPresent(Bool.self, forKey: ._comment_gestures_tapToCollapse) ?? true\n        self.comment_jumpButton = try container.decodeIfPresent(CommentJumpButtonLocation.self, forKey: ._comment_jumpButton) ?? .bottomTrailing\n        self.comment_showCreatorInstance = try container.decodeIfPresent(Bool.self, forKey: ._comment_showCreatorInstance) ?? true\n        self.comment_showDownvotesCompact = try container.decodeIfPresent(Bool.self, forKey: ._comment_showDownvotesCompact) ?? false\n        \n        if let value = try container.decodeIfPresent(Int.self, forKey: ._comment_maxDepth) {\n            self.comment_maxDepth = value\n        } else if let value = try container.decodeIfPresent(Bool.self, forKey: ._comment_behaviors_collapseChildren) {\n            self.comment_maxDepth = value ? 1 : 8\n        } else {\n            self.comment_maxDepth = 8\n        }\n        self.community_showAvatar = try container.decodeIfPresent(Bool.self, forKey: ._community_showAvatar) ?? true\n        self.community_showBanner = try container.decodeIfPresent(Bool.self, forKey: ._community_showBanner) ?? true\n        self.community_showInstance = try container.decodeIfPresent(Bool.self, forKey: ._community_showInstance) ?? true\n        self.comment_createImage_showPost = try container.decodeIfPresent(Bool.self, forKey: ._comment_createImage_showPost) ?? true\n        self.comment_createImage_showCreator = try container.decodeIfPresent(Bool.self, forKey: ._comment_createImage_showCreator) ?? true\n        self.comment_createImage_showStats = try container.decodeIfPresent(Bool.self, forKey: ._comment_createImage_showStats) ?? true\n        self.comment_createImage_colorScheme = try container.decodeIfPresent(UIUserInterfaceStyle.self, forKey: ._comment_createImage_colorScheme) ?? .unspecified\n        self.dev_developerMode = try container.decodeIfPresent(Bool.self, forKey: ._dev_developerMode) ?? false\n        self.dev_errorTimeout = try container.decodeIfPresent(Double.self, forKey: ._dev_errorTimeout) ?? 1.5\n        self.feed_default = try container.decodeIfPresent(ListingType.self, forKey: ._feed_default) ?? .subscribed\n        self.feed_markReadOnScroll = try container.decodeIfPresent(Bool.self, forKey: ._feed_markReadOnScroll) ?? false\n        self.feed_showRead = try container.decodeIfPresent(Bool.self, forKey: ._feed_showRead) ?? true\n        \n        if let tab_inbox_badgeIncludedTypes = try container.decodeIfPresent(Set<InboxItemType>.self, forKey: ._tab_inbox_badgeIncludedTypes) {\n            self.tab_inbox_badgeIncludedTypes = tab_inbox_badgeIncludedTypes\n        } else {\n            let inbox_badge_includeApplications: Bool? = try container.decodeIfPresent(Bool.self, forKey: .inbox_badge_includeApplications)\n            let inbox_badge_includeMessageReports: Bool? = try container.decodeIfPresent(Bool.self, forKey: .inbox_badge_includeMessageReports)\n            let inbox_badge_includeMod: Bool? = try container.decodeIfPresent(Bool.self, forKey: .inbox_badge_includeMod)\n            let inbox_badge_includePersonal: Bool? = try container.decodeIfPresent(Bool.self, forKey: .inbox_badge_includePersonal)\n            var includedTypes: Set<InboxItemType> = []\n            if inbox_badge_includePersonal ?? true {\n                includedTypes.formUnion([.reply, .mention, .message])\n            }\n            if inbox_badge_includeMod ?? true {\n                includedTypes.formUnion([.postReport, .commentReport])\n            }\n            if inbox_badge_includeMessageReports ?? true {\n                includedTypes.formUnion([.messageReport])\n            }\n            if inbox_badge_includeApplications ?? true {\n                includedTypes.insert(.registrationApplication)\n            }\n            self.tab_inbox_badgeIncludedTypes = includedTypes\n        }\n        self.inbox_showRead = try container.decodeIfPresent(Bool.self, forKey: ._inbox_showRead) ?? true\n        self.links_displayMode = try container.decodeIfPresent(TappableLinksDisplayMode.self, forKey: ._links_displayMode) ?? .contextual\n        self.links_openInBrowser = try container.decodeIfPresent(Bool.self, forKey: ._links_openInBrowser) ?? false\n        self.links_readerMode = try container.decodeIfPresent(Bool.self, forKey: ._links_readerMode) ?? false\n        self.links_shareMode = try container.decodeIfPresent(LinkSharingMode.self, forKey: ._links_shareMode) ?? .myInstance\n        self.links_embedLoops = try container.decodeIfPresent(Bool.self, forKey: ._links_embedLoops) ?? true\n\n        // This was only used in a 2.5 beta; remove me\n        let showOverlayByDefault = try container.decodeIfPresent(Bool.self, forKey: ._imageViewer_showOverlayByDefault) ?? true\n        let showControlsOldValue: ShowImageViewerControls = showOverlayByDefault ? .immediately : .onTap\n\n        self.imageViewer_showControls = try container.decodeIfPresent(ShowImageViewerControls.self, forKey: ._imageViewer_showControls) ?? showControlsOldValue\n\n        self.imageViewer_showCloseButton = try container.decodeIfPresent(Bool.self, forKey: ._imageViewer_showCloseButton) ?? true\n        self.imageViewer_showZoomIndicator = try container.decodeIfPresent(Bool.self, forKey: ._imageViewer_showZoomIndicator) ?? true\n        self.imageViewer_dismissThreshold = try container.decodeIfPresent(Int.self, forKey: ._imageViewer_dismissThreshold) ?? 10\n        self.media_animatedAvatars = try container.decodeIfPresent(AnimatedAvatarBehavior.self, forKey: ._media_animatedAvatars) ?? (UIAccessibility.isReduceMotionEnabled ? .never : .always)\n        self.menus_allModActions = try container.decodeIfPresent(Bool.self, forKey: ._menus_allModActions) ?? false\n        self.menus_modActionGrouping = try container.decodeIfPresent(ModeratorActionGrouping.self, forKey: ._menus_modActionGrouping) ?? .divider\n        self.post_defaultSort = try container.decodeIfPresent(LemmySortType.self, forKey: ._post_defaultSort) ?? .hot\n        self.post_fallbackSort = try container.decodeIfPresent(LemmySortType.self, forKey: ._post_fallbackSort) ?? .hot\n        self.post_limitImageHeight = try container.decodeIfPresent(Bool.self, forKey: ._post_limitImageHeight) ?? true\n        self.post_showCreator = try container.decodeIfPresent(Bool.self, forKey: ._post_showCreator) ?? true\n        self.post_showCreatorInstance = try container.decodeIfPresent(Bool.self, forKey: ._post_showCreatorInstance) ?? true\n        self.post_showSubscribedStatus = try container.decodeIfPresent(Bool.self, forKey: ._post_showSubscribedStatus) ?? false\n        self.post_showWebsitePreview = try container.decodeIfPresent(Bool.self, forKey: ._post_showWebsitePreview) ?? true\n        self.post_showDownvotesCompact = try container.decodeIfPresent(Bool.self, forKey: ._post_showDownvotesCompact) ?? false\n        self.post_size = try container.decodeIfPresent(PostSize.self, forKey: ._post_size) ?? .large\n        self.post_allowMultipleColumns = try container.decodeIfPresent(Bool.self, forKey: ._post_allowMultipleColumns) ?? true\n        self.post_thumbnailLocation = try container.decodeIfPresent(ThumbnailLocation.self, forKey: ._post_thumbnailLocation) ?? .left\n        self.post_webPreview_showHost = try container.decodeIfPresent(Bool.self, forKey: ._post_webPreview_showHost) ?? true\n        self.post_webPreview_showIcon = try container.decodeIfPresent(Bool.self, forKey: ._post_webPreview_showIcon) ?? true\n        self.post_gestures_tapToCollapse = try container.decodeIfPresent(Bool.self, forKey: ._post_gestures_tapToCollapse) ?? true\n        self.post_createImage_showCommunity = try container.decodeIfPresent(Bool.self, forKey: ._post_createImage_showCommunity) ?? true\n        self.post_createImage_showCreator = try container.decodeIfPresent(Bool.self, forKey: ._post_createImage_showCreator) ?? true\n        self.post_createImage_showStats = try container.decodeIfPresent(Bool.self, forKey: ._post_createImage_showStats) ?? true\n        self.post_createImage_colorScheme = try container.decodeIfPresent(UIUserInterfaceStyle.self, forKey: ._post_createImage_colorScheme) ?? .unspecified\n        self.privacy_autoBypassImageProxy = try container.decodeIfPresent(Bool.self, forKey: ._privacy_autoBypassImageProxy) ?? false\n        self.privacy_showFavicons = try container.decodeIfPresent(Bool.self, forKey: ._privacy_showFavicons) ?? true\n        self.profile_showBanner = try container.decodeIfPresent(Bool.self, forKey: ._profile_showBanner) ?? true\n        self.safety_blurNsfw = try container.decodeIfPresent(NsfwBlurBehavior.self, forKey: ._safety_blurNsfw) ?? .always\n        self.safety_enableModlogWarning = try container.decodeIfPresent(Bool.self, forKey: ._safety_enableModlogWarning) ?? true\n        self.safety_enableNsfwCommunityWarning = try container.decodeIfPresent(Bool.self, forKey: ._safety_enableNsfwCommunityWarning) ?? true\n        self.tab_gestures_enableLongPress = try container.decodeIfPresent(Bool.self, forKey: ._tab_gestures_enableLongPress) ?? true\n        self.tab_gestures_enableSwipeUp = try container.decodeIfPresent(Bool.self, forKey: ._tab_gestures_enableSwipeUp) ?? true\n        self.tab_gestures_longPressAction = try container.decodeIfPresent(TabBarLongPressAction.self, forKey: ._tab_gestures_longPressAction) ?? .openAccountSwitcher\n        self.tab_profile_labelType = try container.decodeIfPresent(ProfileTabLabel.self, forKey: ._tab_profile_labelType) ?? .nickname\n        self.tab_profile_showAvatar = try container.decodeIfPresent(Bool.self, forKey: ._tab_profile_showAvatar) ?? true\n        self.tab_showNames = try container.decodeIfPresent(Bool.self, forKey: ._tab_showNames) ?? true\n        self.tip_feedWelcomePrompt = try container.decodeIfPresent(Bool.self, forKey: ._tip_feedWelcomePrompt) ?? true\n        self.person_showAvatar = try container.decodeIfPresent(Bool.self, forKey: ._person_showAvatar) ?? true\n        self.person_showInstance = try container.decodeIfPresent(Bool.self, forKey: ._person_showInstance) ?? true\n        self.person_ageVisibility = try container.decodeIfPresent(AccountAgeFlairVisibility.self, forKey: ._person_ageVisibility) ?? .newAccountsOnly\n        self.status_bypassImageProxyShown = try container.decodeIfPresent(Bool.self, forKey: ._status_bypassImageProxyShown) ?? false\n        self.subscriptions_instanceLocation = try container.decodeIfPresent(InstanceLocation.self, forKey: ._subscriptions_instanceLocation) ?? (UIDevice.isPad ? .bottom : .trailing)\n        self.subscriptions_sort = try container.decodeIfPresent(SubscriptionListSort.self, forKey: ._subscriptions_sort) ?? .alphabetical\n        self.navigation_sidebarVisibleByDefault = try container.decodeIfPresent(Bool.self, forKey: ._navigation_sidebarVisibleByDefault) ?? true\n        self.navigation_swipeAnywhere = try container.decodeIfPresent(Bool.self, forKey: ._navigation_swipeAnywhere) ?? false\n        self.filters_keywordFilterEnabled = try container.decodeIfPresent(Bool.self, forKey: ._filters_keywordFilterEnabled) ?? true\n        self.filters_keywords = try container.decodeIfPresent(Set<String>.self, forKey: ._filters_keywords) ?? .init()\n        self.filters_literalFilterEnabled = try container.decodeIfPresent(Bool.self, forKey: ._filters_literalFilterEnabled) ?? true\n        self.filters_literals = try container.decodeIfPresent(Set<String>.self, forKey: ._filters_literals) ?? .init()\n        self.interactionBar_post = try container.decodeIfPresent(PostBarConfiguration.self, forKey: ._interactionBar_post) ?? .default\n        self.interactionBar_comment = try container.decodeIfPresent(CommentBarConfiguration.self, forKey: ._interactionBar_comment) ?? .default\n        self.interactionBar_reply = try container.decodeIfPresent(ReplyBarConfiguration.self, forKey: ._interactionBar_reply) ?? .default\n        self.interactionBar_community = try container.decodeIfPresent(CommunityActionConfiguration.self, forKey: ._interactionBar_community) ?? .init()\n        self.interactionBar_postReport = try container.decodeIfPresent(PostBarConfiguration.self, forKey: ._interactionBar_postReport) ?? .reportDefault_\n        self.interactionBar_commentReport = try container.decodeIfPresent(CommentBarConfiguration.self, forKey: ._interactionBar_commentReport) ?? .reportDefault_\n        self.interactionBar_alternateReportLayout = try container.decodeIfPresent(Bool.self, forKey: ._interactionBar_alternateReportLayout) ?? false\n\n        self.events_showEvents = try container.decodeIfPresent(Bool.self, forKey: ._events_showEvents) ?? true\n    }\n    \n    func reinit(from otherValues: SettingsValues) {\n        a11y_readPostIndicator = otherValues.a11y_readPostIndicator\n        a11y_readOutlineThickness = otherValues.a11y_readOutlineThickness\n        a11y_showSettingsIcons = otherValues.a11y_showSettingsIcons\n        a11y_websiteThumbnailIcon = otherValues.a11y_websiteThumbnailIcon\n        a11y_zoomSliderLocation = otherValues.a11y_zoomSliderLocation\n        accounts_defaultId = otherValues.accounts_defaultId\n        accounts_grouped = otherValues.accounts_grouped\n        accounts_sort = otherValues.accounts_sort\n        accounts_keepPlace = otherValues.accounts_keepPlace\n        accounts_preferredListRowComplication = otherValues.accounts_preferredListRowComplication\n        appearance_interfaceStyle = otherValues.appearance_interfaceStyle\n        appearance_palette = otherValues.appearance_palette\n        markdown_wrapCodeBlockLines = otherValues.markdown_wrapCodeBlockLines\n        behavior_biometricUnlock = otherValues.behavior_biometricUnlock\n        behavior_confirmImageUploads = otherValues.behavior_confirmImageUploads\n        behavior_enableQuickSwipes = otherValues.behavior_enableQuickSwipes\n        behavior_hapticLevel = otherValues.behavior_hapticLevel\n        behavior_internetSpeed = otherValues.behavior_internetSpeed\n        behavior_upvoteOnSave = otherValues.behavior_upvoteOnSave\n        behavior_autoplayMedia = otherValues.behavior_autoplayMedia\n        behavior_muteVideos = otherValues.behavior_muteVideos\n        behavior_infiniteScroll = otherValues.behavior_infiniteScroll\n        comment_behaviors_collapseChildren = otherValues.comment_behaviors_collapseChildren\n        comment_compact = otherValues.comment_compact\n        comment_defaultSort = otherValues.comment_defaultSort\n        comment_gestures_tapToCollapse = otherValues.comment_gestures_tapToCollapse\n        comment_jumpButton = otherValues.comment_jumpButton\n        comment_showCreatorInstance = otherValues.comment_showCreatorInstance\n        comment_maxDepth = otherValues.comment_maxDepth\n        comment_createImage_showPost = otherValues.comment_createImage_showPost\n        comment_createImage_showCreator = otherValues.comment_createImage_showCreator\n        comment_createImage_showStats = otherValues.comment_createImage_showStats\n        comment_createImage_colorScheme = otherValues.comment_createImage_colorScheme\n        comment_showDownvotesCompact = otherValues.comment_showDownvotesCompact\n        community_showAvatar = otherValues.community_showAvatar\n        community_showBanner = otherValues.community_showBanner\n        community_showInstance = otherValues.community_showInstance\n        dev_developerMode = otherValues.dev_developerMode\n        feed_default = otherValues.feed_default\n        feed_markReadOnScroll = otherValues.feed_markReadOnScroll\n        feed_showRead = otherValues.feed_showRead\n        inbox_showRead = otherValues.inbox_showRead\n        links_displayMode = otherValues.links_displayMode\n        links_openInBrowser = otherValues.links_openInBrowser\n        links_readerMode = otherValues.links_readerMode\n        links_shareMode = otherValues.links_shareMode\n        links_embedLoops = otherValues.links_embedLoops\n        imageViewer_showControls = otherValues.imageViewer_showControls\n        imageViewer_showCloseButton = otherValues.imageViewer_showCloseButton\n        imageViewer_showZoomIndicator = otherValues.imageViewer_showZoomIndicator\n        imageViewer_dismissThreshold = otherValues.imageViewer_dismissThreshold\n        media_animatedAvatars = otherValues.media_animatedAvatars\n        menus_allModActions = otherValues.menus_allModActions\n        menus_modActionGrouping = otherValues.menus_modActionGrouping\n        post_defaultSort = otherValues.post_defaultSort\n        post_fallbackSort = otherValues.post_fallbackSort\n        post_limitImageHeight = otherValues.post_limitImageHeight\n        post_showCreator = otherValues.post_showCreator\n        post_showCreatorInstance = otherValues.post_showCreatorInstance\n        post_showSubscribedStatus = otherValues.post_showSubscribedStatus\n        post_showWebsitePreview = otherValues.post_showWebsitePreview\n        post_size = otherValues.post_size\n        post_allowMultipleColumns = otherValues.post_allowMultipleColumns\n        post_thumbnailLocation = otherValues.post_thumbnailLocation\n        post_webPreview_showHost = otherValues.post_webPreview_showHost\n        post_webPreview_showIcon = otherValues.post_webPreview_showIcon\n        post_showDownvotesCompact = otherValues.post_showDownvotesCompact\n        post_gestures_tapToCollapse = otherValues.post_gestures_tapToCollapse\n        post_createImage_showCommunity = otherValues.post_createImage_showCommunity\n        post_createImage_showCreator = otherValues.post_createImage_showCreator\n        post_createImage_showStats = otherValues.post_createImage_showStats\n        post_createImage_colorScheme = otherValues.post_createImage_colorScheme\n        profile_showBanner = otherValues.profile_showBanner\n        privacy_autoBypassImageProxy = otherValues.privacy_autoBypassImageProxy\n        privacy_showFavicons = otherValues.privacy_showFavicons\n        safety_blurNsfw = otherValues.safety_blurNsfw\n        safety_enableModlogWarning = otherValues.safety_enableModlogWarning\n        safety_enableNsfwCommunityWarning = otherValues.safety_enableNsfwCommunityWarning\n        tab_gestures_enableLongPress = otherValues.tab_gestures_enableLongPress\n        tab_gestures_enableSwipeUp = otherValues.tab_gestures_enableSwipeUp\n        tab_profile_labelType = otherValues.tab_profile_labelType\n        tab_profile_showAvatar = otherValues.tab_profile_showAvatar\n        tab_inbox_badgeIncludedTypes = otherValues.tab_inbox_badgeIncludedTypes\n        tab_showNames = otherValues.tab_showNames\n        tip_feedWelcomePrompt = otherValues.tip_feedWelcomePrompt\n        person_showAvatar = otherValues.person_showAvatar\n        person_showInstance = otherValues.person_showInstance\n        person_ageVisibility = otherValues.person_ageVisibility\n        status_bypassImageProxyShown = otherValues.status_bypassImageProxyShown\n        subscriptions_instanceLocation = otherValues.subscriptions_instanceLocation\n        subscriptions_sort = otherValues.subscriptions_sort\n        navigation_sidebarVisibleByDefault = otherValues.navigation_sidebarVisibleByDefault\n        navigation_swipeAnywhere = otherValues.navigation_swipeAnywhere\n        filters_keywordFilterEnabled = otherValues.filters_keywordFilterEnabled\n        filters_keywords = otherValues.filters_keywords\n        filters_literalFilterEnabled = otherValues.filters_literalFilterEnabled\n        filters_literals = otherValues.filters_literals\n        interactionBar_post = otherValues.interactionBar_post\n        interactionBar_comment = otherValues.interactionBar_comment\n        interactionBar_reply = otherValues.interactionBar_reply\n        interactionBar_community = otherValues.interactionBar_community\n        interactionBar_postReport = otherValues.interactionBar_postReport\n        interactionBar_commentReport = otherValues.interactionBar_commentReport\n        interactionBar_alternateReportLayout = otherValues.interactionBar_alternateReportLayout\n        inbox_badge_includeApplications = otherValues.inbox_badge_includeApplications\n        inbox_badge_includeMessageReports = otherValues.inbox_badge_includeMessageReports\n        inbox_badge_includeMod = otherValues.inbox_badge_includeMod\n        inbox_badge_includePersonal = otherValues.inbox_badge_includePersonal\n    }\n    \n    enum CodingKeys: String, CodingKey {\n        case _a11y_readPostIndicator = \"a11y_readPostIndicator\"\n        case _a11y_readOutlineThickness = \"a11y_readOutlineThickness\"\n        case _a11y_showSettingsIcons = \"a11y_showSettingsIcons\"\n        case _a11y_websiteThumbnailIcon = \"a11y_websiteThumbnailIcon\"\n        case _a11y_zoomSliderLocation = \"a11y_zoomSliderLocation\"\n        case _a11y_showInteractionBarButtonBackground = \"a11y_showInteractionBarButtonBackground\"\n        case _accounts_defaultId = \"accounts_defaultId\"\n        case _accounts_grouped = \"accounts_grouped\"\n        case _accounts_sort = \"accounts_sort\"\n        case _accounts_keepPlace = \"accounts_keepPlace\"\n        case _accounts_preferredListRowComplication\n        case _appearance_interfaceStyle = \"appearance_interfaceStyle\"\n        case _appearance_palette = \"appearance_palette\"\n        case _markdown_wrapCodeBlockLines = \"markdown_wrapCodeBlockLines\"\n        case _behavior_biometricUnlock = \"behavior_biometricUnlock\"\n        case _behavior_confirmImageUploads = \"behavior_confirmImageUploads\"\n        case _behavior_enableQuickSwipes = \"behavior_enableQuickSwipes\"\n        case _behavior_hapticLevel = \"behavior_hapticLevel\"\n        case _behavior_internetSpeed = \"behavior_internetSpeed\"\n        case _behavior_upvoteOnSave = \"behavior_upvoteOnSave\"\n        case _behavior_autoplayMedia = \"behavior_autoplayMedia\"\n        case _behavior_muteVideos = \"behavior_muteVideos\"\n        case _behavior_infiniteScroll = \"behavior_infiniteScroll\"\n        case _comment_behaviors_collapseChildren = \"comment_behaviors_collapseChildren\"\n        case _comment_compact = \"comment_compact\"\n        case _comment_defaultSort = \"comment_defaultSort\"\n        case _comment_gestures_tapToCollapse = \"comment_gestures_tapToCollapse\"\n        case _comment_jumpButton = \"comment_jumpButton\"\n        case _comment_showCreatorInstance = \"comment_showCreatorInstance\"\n        case _comment_maxDepth = \"comment_maxDepth\"\n        case _comment_createImage_showPost = \"comment_createImage_showPost\"\n        case _comment_createImage_showCreator = \"comment_createImage_showCreator\"\n        case _comment_createImage_showStats = \"comment_createImage_showStats\"\n        case _comment_createImage_colorScheme = \"comment_createImage_colorScheme\"\n        case _comment_showDownvotesCompact = \"comment_showDownvotesCompact\"\n        case _community_showAvatar = \"community_showAvatar\"\n        case _community_showBanner = \"community_showBanner\"\n        case _community_showInstance = \"community_showInstance\"\n        case _dev_developerMode = \"dev_developerMode\"\n        case _dev_errorTimeout = \"dev_errorTimeout\"\n        case _feed_default = \"feed_default\"\n        case _feed_markReadOnScroll = \"feed_markReadOnScroll\"\n        case _feed_showRead = \"feed_showRead\"\n        case _inbox_showRead = \"inbox_showRead\"\n        case _links_displayMode = \"links_displayMode\"\n        case _links_openInBrowser = \"links_openInBrowser\"\n        case _links_readerMode = \"links_readerMode\"\n        case _links_shareMode = \"links_shareMode\"\n        case _links_embedLoops = \"links_embedLoops\"\n        case _imageViewer_showControls = \"imageViewer_showControls\"\n\n        // This was only used in a 2.5 beta, remove me\n        case _imageViewer_showOverlayByDefault = \"imageViewer_showOverlayByDefault\"\n\n        case _imageViewer_showCloseButton = \"imageViewer_showCloseButton\"\n        case _imageViewer_showZoomIndicator = \"imageViewer_showZoomIndicator\"\n        case _imageViewer_dismissThreshold = \"imageViewer_dismissThreshold\"\n        case _media_animatedAvatars = \"media_animatedAvatars\"\n        case _menus_allModActions = \"menus_allModActions\"\n        case _menus_modActionGrouping = \"menus_modActionGrouping\"\n        case _post_defaultSort = \"post_defaultSort\"\n        case _post_fallbackSort = \"post_fallbackSort\"\n        case _post_limitImageHeight = \"post_limitImageHeight\"\n        case _post_showCreator = \"post_showCreator\"\n        case _post_showCreatorInstance = \"post_showCreatorInstance\"\n        case _post_showSubscribedStatus = \"post_showSubscribedStatus\"\n        case _post_showWebsitePreview = \"post_showWebsitePreview\"\n        case _post_size = \"post_size\"\n        case _post_allowMultipleColumns = \"post_allowMultipleColumns\"\n        case _post_thumbnailLocation = \"post_thumbnailLocation\"\n        case _post_webPreview_showHost = \"post_webPreview_showHost\"\n        case _post_webPreview_showIcon = \"post_webPreview_showIcon\"\n        case _post_showDownvotesCompact = \"post_showDownvotesCompact\"\n        case _post_gestures_tapToCollapse = \"post_gestures_tapToCollapse\"\n        case _post_createImage_showCommunity = \"post_createImage_showCommunity\"\n        case _post_createImage_showCreator = \"post_createImage_showCreator\"\n        case _post_createImage_showStats = \"post_createImage_showStats\"\n        case _post_createImage_colorScheme = \"post_createImage_colorScheme\"\n        case _profile_showBanner = \"profile_showBanner\"\n        case _privacy_autoBypassImageProxy = \"privacy_autoBypassImageProxy\"\n        case _privacy_showFavicons = \"privacy_showFavicons\"\n        case _safety_blurNsfw = \"safety_blurNsfw\"\n        case _safety_enableModlogWarning = \"safety_enableModlogWarning\"\n        case _safety_enableNsfwCommunityWarning = \"safety_enableNsfwCommunityWarning\"\n        case _tab_gestures_enableLongPress = \"tab_gestures_enableLongPress\"\n        case _tab_gestures_enableSwipeUp = \"tab_gestures_enableSwipeUp\"\n        case _tab_gestures_longPressAction = \"tab_gestures_longPressAction\"\n        case _tab_profile_labelType = \"tab_profile_labelType\"\n        case _tab_profile_showAvatar = \"tab_profile_showAvatar\"\n        case _tab_inbox_badgeIncludedTypes = \"tab_inbox_badgeIncludedTypes\"\n        case _tab_showNames = \"tab_showNames\"\n        case _tip_feedWelcomePrompt = \"tip_feedWelcomePrompt\"\n        case _person_showAvatar = \"person_showAvatar\"\n        case _person_showInstance = \"person_showInstance\"\n        case _person_ageVisibility = \"person_ageVisibility\"\n        case _status_bypassImageProxyShown = \"status_bypassImageProxyShown\"\n        case _subscriptions_instanceLocation = \"subscriptions_instanceLocation\"\n        case _subscriptions_sort = \"subscriptions_sort\"\n        case _navigation_sidebarVisibleByDefault = \"navigation_sidebarVisibleByDefault\"\n        case _navigation_swipeAnywhere = \"navigation_swipeAnywhere\"\n        case _filters_keywordFilterEnabled = \"filters_keywordFilterEnabled\"\n        case _filters_keywords = \"filters_keywords\"\n        case _filters_literalFilterEnabled = \"filters_literalFilterEnabled\"\n        case _filters_literals = \"filters_literals\"\n        case _interactionBar_post = \"interactionBar_post\"\n        case _interactionBar_comment = \"interactionBar_comment\"\n        case _interactionBar_reply = \"interactionBar_reply\"\n        case _interactionBar_community = \"interactionBar_community\"\n        case _interactionBar_postReport = \"interactionBar_postReport\"\n        case _interactionBar_commentReport = \"interactionBar_commentReport\"\n        case _interactionBar_alternateReportLayout = \"interactionBar_alternateReportLayout\"\n        case _events_showEvents = \"events_showEvents\"\n\n        case inbox_badge_includeApplications\n        case inbox_badge_includeMessageReports\n        case inbox_badge_includeMod\n        case inbox_badge_includePersonal\n    }\n    \n    init(from settings: LegacySettings, filteredKeywords: Set<String>) {\n        @Dependency(\\.persistenceRepository) var persistenceRepository\n        \n        self.a11y_readPostIndicator = settings.readPostIndicator\n        self.a11y_readOutlineThickness = settings.readOutlineThickness\n        self.a11y_showSettingsIcons = settings.showSettingsIcons\n        self.a11y_websiteThumbnailIcon = settings.websiteThumbnailIcon\n        self.a11y_zoomSliderLocation = settings.zoomSliderLocation\n        self.a11y_showInteractionBarButtonBackground = false\n        self.accounts_defaultId = nil // In 2.0, the last used account is now activated when the app is opened\n        self.accounts_grouped = settings.groupAccountSort\n        self.accounts_sort = settings.accountSort\n        self.accounts_keepPlace = settings.keepPlaceOnAccountSwitch\n        self.accounts_preferredListRowComplication = .lastUsed\n        self.appearance_interfaceStyle = settings.interfaceStyle\n        self.appearance_palette = settings.colorPalette\n        self.markdown_wrapCodeBlockLines = settings.wrapCodeBlockLines\n        self.behavior_biometricUnlock = false // Removed in 2.0\n        self.behavior_confirmImageUploads = settings.confirmImageUploads\n        self.behavior_enableQuickSwipes = settings.quickSwipesEnabled\n        self.behavior_hapticLevel = settings.hapticLevel\n        self.behavior_internetSpeed = settings.internetSpeed\n        self.behavior_upvoteOnSave = settings.upvoteOnSave\n        self.behavior_autoplayMedia = settings.autoplayMedia\n        self.behavior_muteVideos = settings.muteVideos\n        self.behavior_infiniteScroll = settings.infiniteScroll\n        self.comment_behaviors_collapseChildren = false // Replaced by comment_maxDepth in 2.0\n        self.comment_compact = settings.compactComments\n        self.comment_defaultSort = settings.commentSort\n        self.comment_gestures_tapToCollapse = settings.tapCommentsToCollapse\n        self.comment_jumpButton = settings.jumpButton\n        self.comment_showCreatorInstance = true // Removed in 2.0\n        self.comment_maxDepth = settings.maxCommentDepth\n        self.comment_createImage_showPost = true // Added in 2.4\n        self.comment_createImage_showCreator = true // Added in 2.4\n        self.comment_createImage_showStats = true // Added in 2.4\n        self.comment_createImage_colorScheme = .unspecified // Added in 2.4\n        self.comment_showDownvotesCompact = false // Added in 2.5\n        self.community_showAvatar = settings.showCommunityAvatar\n        self.community_showBanner = true // Removed in 2.0\n        self.community_showInstance = true // Removed in 2.0\n        self.dev_developerMode = settings.developerMode\n        self.dev_errorTimeout = 1.5 // Added in 2.5\n        self.feed_default = settings.defaultFeed\n        self.feed_markReadOnScroll = settings.markReadOnScroll\n        self.feed_showRead = settings.showReadInFeed\n        self.inbox_showRead = settings.showReadInInbox\n        self.links_displayMode = settings.tappableLinksDisplayMode\n        self.links_openInBrowser = settings.openLinksInBrowser\n        self.links_readerMode = settings.openLinksInReaderMode\n        self.links_shareMode = settings.linkSharingMode\n        self.links_embedLoops = settings.embedLoops\n        self.imageViewer_showControls = .immediately // Added in 2.5\n        self.imageViewer_showCloseButton = true // Added in 2.5\n        self.imageViewer_showZoomIndicator = true // Added in 2.5\n        self.imageViewer_dismissThreshold = 10 // Added in 2.5\n        self.media_animatedAvatars = settings.animatedAvatars\n        self.menus_allModActions = settings.showAllModActions\n        self.menus_modActionGrouping = settings.moderatorActionGrouping\n        self.post_defaultSort = settings.defaultPostSort\n        self.post_fallbackSort = settings.fallbackPostSort\n        self.post_limitImageHeight = true // Removed in 2.0\n        self.post_showCreator = settings.showPostCreator\n        self.post_showCreatorInstance = true // Removed in 2.0\n        self.post_showSubscribedStatus = settings.showSubscribedStatus\n        self.post_showWebsitePreview = true // Removed in 2.0\n        self.post_size = settings.postSize\n        self.post_allowMultipleColumns = settings.allowMultiplePostColumns\n        self.post_thumbnailLocation = settings.thumbnailLocation\n        self.post_webPreview_showHost = true // Removed in 2.0\n        self.post_webPreview_showIcon = settings.showFavicons\n        self.post_showDownvotesCompact = settings.showDownvotesCompact\n        self.post_gestures_tapToCollapse = true\n        self.post_createImage_showCommunity = true // Added in 2.4\n        self.post_createImage_showCreator = true // Added in 2.4\n        self.post_createImage_showStats = true // Added in 2.4\n        self.post_createImage_colorScheme = .unspecified // Added in 2.4\n        self.profile_showBanner = true // Removed in 2.0\n        self.safety_blurNsfw = settings.blurNsfw\n        self.safety_enableNsfwCommunityWarning = settings.showNsfwCommunityWarning\n        self.safety_enableModlogWarning = settings.showModlogWarning\n        self.tab_gestures_enableLongPress = true // Removed in 2.0\n        self.tab_gestures_enableSwipeUp = true // Removed in 2.0\n        self.tab_gestures_longPressAction = .openAccountSwitcher // Added in 2.2\n        self.tab_profile_labelType = settings.tabProfileLabelType\n        self.tab_profile_showAvatar = settings.tabProfileShowAvatar\n        self.tab_inbox_badgeIncludedTypes = settings.tabInboxBadgeIncludedTypes\n        self.tab_showNames = true // Removed in 2.0\n        self.tip_feedWelcomePrompt = settings.showFeedWelcomePrompt\n        self.person_showAvatar = settings.showPersonAvatar\n        self.person_showInstance = true // Removed in 2.0\n        self.person_ageVisibility = .newAccountsOnly // Added in 2.2\n        self.privacy_autoBypassImageProxy = settings.autoBypassImageProxy\n        self.privacy_showFavicons = settings.showFavicons // TODO: unused?\n        self.status_bypassImageProxyShown = settings.bypassImageProxyShown\n        self.subscriptions_instanceLocation = settings.subscriptionInstanceLocation\n        self.subscriptions_sort = settings.subscriptionSort\n        self.navigation_sidebarVisibleByDefault = settings.sidebarVisibleByDefault\n        self.navigation_swipeAnywhere = settings.swipeAnywhereToNavigate\n        self.filters_keywordFilterEnabled = settings.keywordFilterEnabled\n        self.filters_keywords = filteredKeywords\n        self.filters_literalFilterEnabled = true // Added in 2.4\n        self.filters_literals = .init() // Added in 2.4\n        self.interactionBar_alternateReportLayout = settings.alternateInteractionBarLayoutForReports\n        \n        let interactionBarConfigurations = persistenceRepository.loadInteractionBarConfigurations()\n        self.interactionBar_post = interactionBarConfigurations.post\n        self.interactionBar_comment = interactionBarConfigurations.comment\n        self.interactionBar_reply = interactionBarConfigurations.reply\n        self.interactionBar_community = .init() // Added in 2.5\n        self.interactionBar_postReport = interactionBarConfigurations.postReport\n        self.interactionBar_commentReport = interactionBarConfigurations.commentReport\n        self.events_showEvents = true // Added in 2.5\n    }\n}\n\n// swiftlint:enable line_length function_body_length file_length\n"
  },
  {
    "path": "Mlem/App/Data/Document.swift",
    "content": "//\n//  Document.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-07-09.\n//\n\nimport Foundation\n\nstruct Document: Identifiable, Hashable {\n    let title: String\n    let body: String\n    var id: Int {\n        var hasher = Hasher()\n        hasher.combine(body)\n        return hasher.finalize()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Data/EULA.swift",
    "content": "//\n//  EULA.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-07-09.\n//\n\nimport Foundation\n\n// swiftlint:disable line_length\nextension Document {\n    static let eula: Document = .init(\n        title: \"EULA\",\n        body: \"\"\"\n        # EULA\n\n        Welcome to Mlem! Before you proceed, please carefully read the following Terms of Service (\"Terms\") governing your use of our app. By accessing or using Mlem, you acknowledge that you have read, understood, and agreed to be bound by these Terms. If you do not agree with any part of these Terms, please refrain from using Mlem.\n\n        ## 1. User Responsibilities\n\n        1.1 Reporting Content: Questionable content or content in violation of the rules and guidelines of the Lemmy instance or community on which it is hosted can be reported to the Lemmy community moderators using Mlem's built-in report function or on the instance website. We are not responsible for moderating or enforcing Lemmy instance or community rules.\n\n        1.2 Blocking Users and Instances: Mlem provides you with the ability to block individual users, communities, or entire instances. We encourage you to use these blocking features to create a safe and enjoyable Mlem experience.\n\n        1.3 Following Instance Rules: You are required to follow the rules of the instance(s) that you access using Mlem. Instance terms of use can be found on the instance website. Failure to comply with the rules of an instance may result in suspension or termination of your account with that instance as dictated by the instance rules.\n\n        1.4 No Misuse: The misuse of Mlem will result in termination of all services--to the furthest of our ability--with us. We reserve the right to terminate--to the furthest of our ability--any services that we provide.\n\n        1.5 Adult Content: Some Lemmy instances that Mlem accesses may host adult content. Lemmy blocks this content by default; if you wish to view it, in-app or otherwise, you can enable the option on the website of the instance where your account is registered. We take reasonable measures to prevent the display of explicit or adult content within Mlem, but we cannot guarantee that all instances or communities will follow the appropriate procedures to label adult content as such. It is your responsibility to exercise caution while accessing external content.\n\n        1.6 No Abusive, Unlawful, or Offensive Content: You may not use Mlem to produce or distribute any abusive, unlawful, or offensive content. This includes, but is not limited to: content that is unlawful, libelous, defamatory, or tortious; harmful, threatening, abusive, invasive, or harassing; or hateful or racially, ethnically, or otherwise discriminatory. The content you produce will not harm minors in any way. You will not impersonate any person or entity. You will not upload or post any content that you do not have the right to make available under any US or foreign laws. You will not produce content that interferes with or disrupts the app, the Lemmy instances that you use, other Lemmy instances, or any other service or person. You will not transmit misinformation in any capacity to any individual.\n\n        ## 2. Limitation of Liability\n\n        2.1 No Liability: To the fullest extent permitted by applicable law, we disclaim any liability for any direct, indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, arising from your use of Mlem or any interactions within the Lemmy instances that you access thereby. This includes, but is not limited to, any damages resulting from the content, actions, or conduct of other Mlem or Lemmy users.\n\n        ## 3. General Provisions\n\n        3.1 Modifications: We reserve the right to modify, suspend, or terminate Mlem or these Terms, at our sole discretion, at any time and without prior notice. Your continued use of Mlem after any modifications to these Terms shall constitute your acceptance of the modified Terms.\n\n        3.2 Governing Law: These Terms shall be governed by and construed in accordance with the laws of the United States, without regard to its conflict of laws principles.\n\n        3.3 Entire Agreement: These Terms constitute the entire agreement between you and us regarding your use of Mlem and supersede any prior or contemporaneous agreements, communications, or proposals, whether oral or written, between you and us.\n\n        By using Mlem, you affirm that you have read, understood, and agreed to these revised Terms of Service. If you have any questions or concerns, please contact us at mlemappofficial@gmail.com. Thank you for using Mlem!\n        \"\"\"\n    )\n}\n\n// swiftlint:enable line_length\n"
  },
  {
    "path": "Mlem/App/Data/Licenses.swift",
    "content": "//\n//  Licenses.swift\n//  Mlem\n//\n//  Created by Weston Hanners on 7/12/23.\n//\n\nimport Foundation\n\n// swiftlint:disable file_length line_length\nextension Document {\n    static let allLicenses: [Document] = [\n        .cmarkLicense,\n        .keychainAccessLicense,\n        .nukeLicense,\n        .semaphoreLicense,\n        .swiftDependenciesLicense,\n        .swiftUiFlowLicense,\n        .swiftUiIntrospectLicence,\n        .swiftUiXLicense,\n        .solarizedThemeLicense,\n        .draculaThemeLicense\n    ]\n    \n    static let keychainAccessLicense = Document(\n        title: \"Keychain Access\",\n        body: \"\"\"\n        The MIT License (MIT)\n        \n        Copyright (c) 2014 kishikawa katsumi\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the \"Software\"), to deal\n        in the Software without restriction, including without limitation the rights\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n        copies of the Software, and to permit persons to whom the Software is\n        furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.\n        \"\"\"\n    )\n    \n    static let nukeLicense = Document(\n        title: \"Nuke\",\n        body: \"\"\"\n        The MIT License (MIT)\n        \n        Copyright (c) 2015-2023 Alexander Grebenyuk\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the \"Software\"), to deal\n        in the Software without restriction, including without limitation the rights\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n        copies of the Software, and to permit persons to whom the Software is\n        furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.\n        \"\"\"\n    )\n    \n    static let swiftDependenciesLicense = Document(\n        title: \"Swift Dependencies\",\n        body: \"\"\"\n        MIT License\n        \n        Copyright (c) 2022 Point-Free, Inc.\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the \"Software\"), to deal\n        in the Software without restriction, including without limitation the rights\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n        copies of the Software, and to permit persons to whom the Software is\n        furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.\n        \"\"\"\n    )\n    \n    static let swiftUiXLicense = Document(\n        title: \"SwiftUIX\",\n        body: \"\"\"\n        Copyright © 2020 Vatsal Manot\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the “Software”), to\n        deal in the Software without restriction, including without limitation the\n         rights to use, copy, modify, merge, publish, distribute, sublicense,\n        and/or sell copies of the Software, and to permit persons to whom the\n        Software is furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included\n        in all copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL\n        THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n        FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n        DEALINGS IN THE SOFTWARE.\n        \"\"\"\n    )\n    \n    static let cmarkLicense = Document(\n        title: \"cmark-gfm\",\n        body: \"\"\"\n        Copyright (c) 2014, John MacFarlane\n        \n        All rights reserved.\n        \n        Redistribution and use in source and binary forms, with or without\n        modification, are permitted provided that the following conditions are met:\n        \n            * Redistributions of source code must retain the above copyright\n              notice, this list of conditions and the following disclaimer.\n        \n            * Redistributions in binary form must reproduce the above\n              copyright notice, this list of conditions and the following\n              disclaimer in the documentation and/or other materials provided\n              with the distribution.\n        \n        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n        \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n        LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n        A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n        OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n        SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n        LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n        DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n        THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n        (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n        OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n        \n        -----\n        \n        houdini.h, houdini_href_e.c, houdini_html_e.c, houdini_html_u.c\n        \n        derive from https://github.com/vmg/houdini (with some modifications)\n        \n        Copyright (C) 2012 Vicent Martí\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy of\n        this software and associated documentation files (the \"Software\"), to deal in\n        the Software without restriction, including without limitation the rights to\n        use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\n        of the Software, and to permit persons to whom the Software is furnished to do\n        so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.\n        \n        -----\n        \n        buffer.h, buffer.c, chunk.h\n        \n        are derived from code (C) 2012 Github, Inc.\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy of\n        this software and associated documentation files (the \"Software\"), to deal in\n        the Software without restriction, including without limitation the rights to\n        use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\n        of the Software, and to permit persons to whom the Software is furnished to do\n        so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.\n        \n        -----\n        \n        utf8.c and utf8.c\n        \n        are derived from utf8proc\n        (<http://www.public-software-group.org/utf8proc>),\n        (C) 2009 Public Software Group e. V., Berlin, Germany.\n        \n        Permission is hereby granted, free of charge, to any person obtaining a\n        copy of this software and associated documentation files (the \"Software\"),\n        to deal in the Software without restriction, including without limitation\n        the rights to use, copy, modify, merge, publish, distribute, sublicense,\n        and/or sell copies of the Software, and to permit persons to whom the\n        Software is furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in\n        all copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n        FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n        DEALINGS IN THE SOFTWARE.\n        \n        -----\n        \n        The normalization code in normalize.py was derived from the\n        markdowntest project, Copyright 2013 Karl Dubost:\n        \n        The MIT License (MIT)\n        \n        Copyright (c) 2013 Karl Dubost\n        \n        Permission is hereby granted, free of charge, to any person obtaining\n        a copy of this software and associated documentation files (the\n        \"Software\"), to deal in the Software without restriction, including\n        without limitation the rights to use, copy, modify, merge, publish,\n        distribute, sublicense, and/or sell copies of the Software, and to\n        permit persons to whom the Software is furnished to do so, subject to\n        the following conditions:\n        \n        The above copyright notice and this permission notice shall be\n        included in all copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n        EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n        MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n        NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n        LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n        OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n        WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n        \n        -----\n        \n        The CommonMark spec (test/spec.txt) is\n        \n        Copyright (C) 2014-15 John MacFarlane\n        \n        Released under the Creative Commons CC-BY-SA 4.0 license:\n        <http://creativecommons.org/licenses/by-sa/4.0/>.\n        \n        -----\n        \n        The test software in test/ is\n        \n        Copyright (c) 2014, John MacFarlane\n        \n        All rights reserved.\n        \n        Redistribution and use in source and binary forms, with or without\n        modification, are permitted provided that the following conditions are met:\n        \n            * Redistributions of source code must retain the above copyright\n              notice, this list of conditions and the following disclaimer.\n        \n            * Redistributions in binary form must reproduce the above\n              copyright notice, this list of conditions and the following\n              disclaimer in the documentation and/or other materials provided\n              with the distribution.\n        \n        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n        \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n        LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n        A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n        OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n        SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n        LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n        DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n        THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n        (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n        OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n        \"\"\"\n    )\n    static let swiftUiFlowLicense = Document(\n        title: \"SwiftUI-Flow\",\n        body: \"\"\"\n        MIT License\n        \n        Copyright (c) 2023 Laszlo Teveli\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the \"Software\"), to deal\n        in the Software without restriction, including without limitation the rights\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n        copies of the Software, and to permit persons to whom the Software is\n        furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.\n        \"\"\"\n    )\n    static let semaphoreLicense = Document(\n        title: \"Semaphore\",\n        body: \"\"\"\n        MIT License\n        \n        Copyright (c) 2022 Gwendal Roué\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the \"Software\"), to deal\n        in the Software without restriction, including without limitation the rights\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n        copies of the Software, and to permit persons to whom the Software is\n        furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.\n        \"\"\"\n    )\n    static let swiftUiIntrospectLicence = Document(\n        title: \"swiftui-introspect\",\n        body: \"\"\"\n        Copyright 2019 Timber Software\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n        \"\"\"\n    )\n    static let solarizedThemeLicense = Document(\n        title: \"Solarized Theme\",\n        body: \"\"\"\n        Copyright (c) 2011 Ethan Schoonover\n\n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the \"Software\"), to deal\n        in the Software without restriction, including without limitation the rights\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n        copies of the Software, and to permit persons to whom the Software is\n        furnished to do so, subject to the following conditions:\n\n        The above copyright notice and this permission notice shall be included in\n        all copies or substantial portions of the Software.\n\n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n        THE SOFTWARE.\n        \"\"\"\n    )\n    static let draculaThemeLicense = Document(\n        title: \"Dracula Theme\",\n        body: \"\"\"\n        The MIT License (MIT)\n\n        Copyright (c) 2023 Dracula Theme\n\n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the \"Software\"), to deal\n        in the Software without restriction, including without limitation the rights\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n        copies of the Software, and to permit persons to whom the Software is\n        furnished to do so, subject to the following conditions:\n\n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n\n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.\n        \"\"\"\n    )\n}\n\n// swiftlint:enable file_length line_length\n"
  },
  {
    "path": "Mlem/App/Data/Privacy Policy.swift",
    "content": "//\n//  Privacy Policy.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-07-09.\n//\n\nimport Foundation\n\n// swiftlint:disable line_length\nextension Document {\n    static let privacyPolicy: Document = .init(\n        title: \"Privacy Policy\",\n        body: \"\"\"\n        # Privacy Policy\n\n        Effective Date: July 15th, 2023\n\n        Thank you for using Mlem! This Privacy Policy outlines how your personal information is collected, used, and protected when you use the Mlem mobile application (\"App\"). Please read this Privacy Policy carefully to understand our practices regarding your personal information. By using the Mlem mobile application, you acknowledge that you have read and understood this Privacy Policy and agree to the collection, use, and disclosure of your information as described herein.\n\n        ## Information We Collect\n        Mlem does not collect or store any user data. We do not collect any personally identifiable information or track your activities within the App.\n\n        ## Servers and Data Control\n        Mlem connects to servers that we do not host or have control over. While we strive to ensure the security and privacy of your data within our App, we cannot guarantee the security or privacy practices of the external servers. Any data stored or processed on these servers is subject to the respective privacy policies and terms of service of those servers.\n\n        ## Third-Party Services\n        Mlem does not integrate any third-party services, advertising networks, or analytics tools that collect personal information or track your activities within the App. We prioritize user privacy and do not engage in any data sharing or tracking practices.\n\n        ## Children's Privacy\n        Mlem is not intended for use by individuals under the age of 13. We do not knowingly collect personal information from children under the age of 13. If we become aware that we have inadvertently collected personal information from a child under the age of 13, we will take steps to delete the information as soon as possible. If you believe that we may have collected information from a child under the age of 13, please contact us using the information provided in the \"Contact Us\" section below.\n\n        ## Changes to this Privacy Policy\n        We reserve the right to modify or update this Privacy Policy at any time. Any changes will be effective immediately upon posting the revised Privacy Policy.\n\n        ## Contact Us\n        If you have any questions, concerns, or requests regarding this Privacy Policy or the privacy practices of Mlem, please contact us at mlemappofficial@gmail.com.\n        \"\"\"\n    )\n}\n\n// swiftlint:enable line_length\n"
  },
  {
    "path": "Mlem/App/Enums/AnimatedAvatarBehavior.swift",
    "content": "//\n//  AnimatedAvatarBehavior.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-15.\n//\n\nimport Foundation\nimport Icons\n\nenum AnimatedAvatarBehavior: String, CaseIterable, Codable {\n    case always, profile, never\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .always: \"Always\"\n        case .profile: \"Only in Profile\"\n        case .never: \"Never\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .always: .general.success\n        case .profile: .lemmy.person\n        case .never: .general.failure\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/AvatarType.swift",
    "content": "//\n//  AvatarType.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-10-02.\n//\n\nimport Foundation\nimport Icons\n\n// TODO: move into DefaultAvatarView?\n/// Enum of things that can have avatars\nenum AvatarType {\n    case person, community, instance\n}\n"
  },
  {
    "path": "Mlem/App/Enums/CommentJumpButtonLocation.swift",
    "content": "//\n//  CommentJumpButtonLocation.swift\n//  Mlem\n//\n//  Created by Sjmarf on 24/08/2024.\n//\n\nimport Icons\nimport SwiftUI\n\nenum CommentJumpButtonLocation: String, CaseIterable, Codable {\n    case bottomLeading, bottomTrailing, bottomCenter, none\n    \n    var alignment: Alignment {\n        switch self {\n        case .bottomLeading: .bottomLeading\n        case .bottomTrailing: .bottomTrailing\n        case .bottomCenter: .bottom\n        case .none: .bottomTrailing\n        }\n    }\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .bottomLeading: \"Left\"\n        case .bottomTrailing: \"Right\"\n        case .bottomCenter: \"Center\"\n        case .none: \"Hidden\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .bottomLeading: .general.backward\n        case .bottomTrailing: .general.forward\n        default: .settings.center\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/InstanceSort.swift",
    "content": "//\n//  InstanceSort.swift\n//  Mlem\n//\n//  Created by Sjmarf on 09/09/2024.\n//\n\nimport Foundation\nimport Icons\n\nenum InstanceSort: CaseIterable {\n    case alphabetical, score, users, version\n    // TODO: Add \"New\", \"Old\", \"Active Users\"? Requires MlemStats update\n    // We could add a _lot_ of sort modes here if we wanted to once we get a HTTP server (https://github.com/mlemgroup/mlem/issues/1313)\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .alphabetical: \"Alphabetical\"\n        case .score: \"Score\"\n        case .users: \"Users\"\n        case .version: \"Version\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .alphabetical: .lemmy.alphabeticalSort\n        case .score: .lemmy.scoreSort\n        case .users: .lemmy.usersSort\n        case .version: .lemmy.versionSort\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/ActionSeedSections.swift",
    "content": "//\n//  ActionSeedSections.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-02-28.\n//\n\nimport Actions\n\nstruct ActionSeedSections {\n    let sections: [[ActionSeed]]\n\n    init(sections: [[ActionSeed]]) {\n        self.sections = sections\n    }\n\n    var all: [ActionSeed] { sections.reduce([], +) }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/CommentBarConfiguration+Types.swift",
    "content": "//\n//  CommentBarConfiguration+Types.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-15.\n//\n\nimport Actions\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nextension CommentBarConfiguration {\n    enum ActionType: String, ActionTypeProviding {\n        typealias Configuration = CommentBarConfiguration // swiftlint:disable:this nesting\n        \n        case upvote\n        case downvote\n        case save\n        case reply\n        case share\n        case selectText\n        case report\n        case resolve\n        case remove\n        case ban\n        case collapse\n        case collapseParent\n        case collapseToTop\n        \n        static var defaultWidgets: [ActionType] { [\n            .upvote,\n            .downvote,\n            .save,\n            .reply,\n            .share\n        ] }\n        \n        static var defaultReportWidgets: [ActionType] { [\n            .share,\n            .resolve,\n            .remove,\n            .ban\n        ] }\n        \n        var appearance: ActionAppearance {\n            switch self {\n            case .upvote: .upvote(isOn: false)\n            case .downvote: .downvote(isOn: false)\n            case .save: .save(isOn: false)\n            case .reply: .reply()\n            case .share: .share()\n            case .selectText: .selectText()\n            case .report: .report()\n            case .resolve: .resolve(isOn: false)\n            case .remove: .remove(isOn: false)\n            case .ban: .banFromCommunity(isOn: false)\n            case .collapse: .collapse()\n            case .collapseParent: .collapseParent()\n            case .collapseToTop: .collapseToTop()\n            }\n        }\n        \n        func associatedReadouts(context: any InteractableProviding) -> Set<Configuration.ReadoutType> {\n            switch self {\n            case .upvote: context.votes.value?.myVote ?? .none == .upvote ? [.upvote, .score] : [.upvote]\n            case .downvote: context.votes.value?.myVote ?? .none == .downvote ? [.downvote, .score] : [.downvote]\n            case .save: [.saved]\n            case .reply, .share, .selectText, .report, .resolve, .remove, .ban: []\n            case .collapse, .collapseParent, .collapseToTop: []\n            }\n        }\n               \n        var actionSeed: ActionSeed {\n            switch self {\n            case .upvote: .upvote\n            case .downvote: .downvote\n            case .save: .save\n            case .reply: .reply\n            case .share: .share\n            case .selectText: .selectText\n            case .report: .report\n            case .resolve: .resolveReport\n            case .remove: .remove\n            case .ban: .ban\n            case .collapse: .collapse\n            case .collapseParent: .collapseParent\n            case .collapseToTop: .collapseToTop\n            }\n        }\n    }\n    \n    enum CounterType: String, CounterTypeProviding {\n        typealias Configuration = CommentBarConfiguration // swiftlint:disable:this nesting\n        \n        case score\n        case upvote\n        case downvote\n        case reply\n        \n        static var defaultWidgets: [CounterType] { allCases }\n        \n        var appearance: CounterAppearance {\n            switch self {\n            case .score: .score()\n            case .upvote: .upvote()\n            case .downvote: .downvote()\n            case .reply: .reply()\n            }\n        }\n        \n        func associatedReadouts(context: any InteractableProviding) -> Set<Configuration.ReadoutType> {\n            switch self {\n            case .score: [.upvote, .downvote, .score]\n            case .upvote: context.votes.value?.myVote ?? .none == .upvote ? [.upvote, .score] : [.upvote]\n            case .downvote: context.votes.value?.myVote ?? .none == .downvote ? [.downvote, .score] : [.downvote]\n            case .reply: []\n            }\n        }\n    }\n    \n    enum ReadoutType: String, ReadoutTypeProviding {\n        case created\n        case score\n        case upvote\n        case downvote\n        case comment\n        case saved\n        \n        var appearance: MockReadoutAppearance {\n            switch self {\n            case .created: .init(icon: .general.time, label: \"18h\")\n            case .score: .init(icon: .lemmy.votes, label: \"7\")\n            case .upvote: .init(icon: .lemmy.upvoted, label: \"9\")\n            case .downvote: .init(icon: .lemmy.downvoted, label: \"2\")\n            case .comment: .init(icon: .lemmy.replies, label: \"1\")\n            case .saved: .init(icon: .lemmy.saved, label: \"\")\n            }\n        }\n        \n        func compatibleWith(otherReadouts: Set<Self>) -> Bool {\n            switch self {\n            case .score: otherReadouts.isDisjoint(with: [.upvote, .downvote])\n            case .upvote, .downvote: !otherReadouts.contains(.score)\n            default: true\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/CommentBarConfiguration.swift",
    "content": "//\n//  CommentInteraction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 14/06/2024.\n//\n\nimport Actions\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CommentBarConfiguration: InteractionBarConfiguration, SwipeActionConfiguration {\n    var leading: [Item]\n    var trailing: [Item]\n    var readouts: [ReadoutType]\n    var savedContextMenu: [ActionSeed]?\n\n    public var savedSwipes: ActionSeedSwipeConfiguration?\n\n    static var defaultSwipes: ActionSeedSwipeConfiguration {\n        .init(leading: [.downvote, .upvote], trailing: [.save, .reply])\n    }\n\n    static var defaultContextMenu: [ActionSeed] {\n        [.selectText, .share, .blockCreator, .report, .edit, .delete, .remove, .banCreator, .resolveReport]\n    }\n\n    var availableWidgets: Set<Item>\n    func widgetPickerPage(_ configuration: Binding<Self>) -> SettingsPage { .commentBarWidgetPicker(configuration) }\n    \n    init(\n        leading: [Item],\n        trailing: [Item],\n        savedSwipes: ActionSeedSwipeConfiguration?,\n        readouts: [ReadoutType],\n        availableWidgets: Set<Item>,\n        savedContextMenu: [ActionSeed]?\n    ) {\n        self.leading = leading\n        self.trailing = trailing\n        self.savedSwipes = savedSwipes\n        self.readouts = readouts\n        self.availableWidgets = availableWidgets\n        self.savedContextMenu = savedContextMenu\n    }\n    \n    init(from decoder: any Decoder) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        self.leading = try container.decodeIfPresent([Item].self, forKey: .leading) ?? [.counter(.score)]\n        self.trailing = try container.decodeIfPresent([Item].self, forKey: .trailing) ?? [.action(.save), .action(.reply)]\n        self.readouts = try container.decodeIfPresent([ReadoutType].self, forKey: .readouts) ?? [.created, .comment]\n        self.availableWidgets = try container.decodeIfPresent(Set<Item>.self, forKey: .availableWidgets) ??\n            .init(CounterType.defaultWidgets.map { .counter($0) } + ActionType.defaultWidgets.map { .action($0) })\n        if let contextMenuKeys = try container.decodeIfPresent([String].self, forKey: .savedContextMenu) {\n            let allActions = Self.availableActions.all\n            self.savedContextMenu = contextMenuKeys.compactMap { key in allActions.first(where: {$0.key == key}) }\n        } else {\n            self.savedContextMenu = nil\n        }\n\n        let swipeConfigurationContainer = try? container.nestedContainer(\n            keyedBy: ActionSeedSwipeConfiguration.CodingKeys.self,\n            forKey: .swipes\n        )\n        if let swipeConfigurationContainer {\n            self.savedSwipes = try .init(from: swipeConfigurationContainer, availableActions: Self.availableActions.all)\n        } else {\n            // Convert from Mlem 2.4 -> 2.5 format\n            let leadingSwipes = try container.decodeIfPresent([ActionType].self, forKey: .leadingSwipes) ?? [.upvote, .downvote]\n            let trailingSwipes = try container.decodeIfPresent([ActionType].self, forKey: .trailingSwipes) ?? [.save, .reply]\n\n            let swipes = ActionSeedSwipeConfiguration(\n                leading: leadingSwipes.map(\\.actionSeed),\n                trailing: trailingSwipes.map(\\.actionSeed)\n            )\n\n            if swipes == Self.defaultSwipes {\n                self.savedSwipes = nil\n            } else {\n                self.savedSwipes = swipes\n            }\n        }\n    }\n    \n    enum CodingKeys: CodingKey {\n        case leading\n        case trailing\n        case readouts \n        case availableWidgets\n        case savedContextMenu\n        case swipes\n\n        // Used for conversion from Mlem 2.4 -> 2.5 format\n        case leadingSwipes\n        case trailingSwipes\n    }\n\n    func encode(to encoder: any Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(self.leading, forKey: .leading)\n        try container.encode(self.trailing, forKey: .trailing)\n        try container.encode(self.readouts, forKey: .readouts)\n        try container.encode(self.availableWidgets, forKey: .availableWidgets)\n        try container.encode(self.savedContextMenu, forKey: .savedContextMenu)\n        try container.encode(self.savedSwipes, forKey: .swipes)\n    }\n\n    static var `default`: Self {\n        .init(\n            leading: [.counter(.score)],\n            trailing: [.action(.save), .action(.reply)],\n            savedSwipes: nil,\n            readouts: [.created, .comment],\n            availableWidgets: .init(CounterType.defaultWidgets.map { .counter($0) } + ActionType.defaultWidgets.map { .action($0) }),\n            savedContextMenu: nil\n        )\n    }\n    \n    static var reportDefault_: Self {\n        .init(\n            leading: [.action(.resolve), .action(.share)],\n            trailing: [.action(.ban), .action(.remove)],\n            savedSwipes: nil,\n            readouts: [.upvote, .downvote, .created, .comment],\n            availableWidgets: .init(ActionType.defaultReportWidgets.map { .action($0) }),\n            savedContextMenu: nil\n        )\n    }\n\n    static var availableActions: ActionSeedSections { .init(sections: [\n            [\n                .upvote,\n                .downvote,\n                .save,\n                .reply,\n                .selectText,\n                .share,\n                .createImage,\n                .report,\n                .edit,\n                .delete\n            ],\n            [\n                .collapse,\n                .collapseParent,\n                .collapseToTop\n            ],\n            [\n                .blockCreator,\n                .copyAuthorName,\n                .openCreatorModlog,\n                .sendCreatorMessage\n            ],\n            [\n                .viewVotes,\n                .remove,\n                .banCreator,\n                .purge,\n                .purgeCreator,\n                .resolveReport\n            ]\n        ])\n    }\n    \n    static var reportDefault: Self? { reportDefault_ }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/CommunityActionConfiguration.swift",
    "content": "//\n//  CommunityActionConfiguration.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-04.\n//\n\nimport Actions\nimport Foundation\n\nstruct CommunityActionConfiguration: Codable, SwipeActionConfiguration {\n    var savedSwipes: ActionSeedSwipeConfiguration?\n\n    static var availableActions: ActionSeedSections { .init(sections: [\n            [\n                .newPost,\n                .subscribe,\n                .favorite,\n                .goToInstance,\n                .copyName,\n                .share\n            ],\n            [\n                .block,\n                .remove,\n                .purge\n            ]\n        ])\n    }\n\n    static var defaultSwipes: ActionSeedSwipeConfiguration {\n        .init(leading: [], trailing: [.subscribe, .favorite])\n    }\n\n    enum CodingKeys: CodingKey {\n        case swipes\n    }\n\n    init() {\n        self.savedSwipes = nil\n    }\n\n    init(from decoder: any Decoder) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        let swipeConfigurationContainer = try? container.nestedContainer(\n            keyedBy: ActionSeedSwipeConfiguration.CodingKeys.self,\n            forKey: .swipes\n        )\n        if let swipeConfigurationContainer {\n            self.savedSwipes = try .init(from: swipeConfigurationContainer, availableActions: Self.availableActions.all)\n        } else {\n            self.savedSwipes = nil\n        }\n    }\n\n    public func encode(to encoder: any Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(self.savedSwipes, forKey: .swipes)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/ContextMenuConfiguration.swift",
    "content": "//\n//  ContextMenuConfiguration.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-21.\n//\n\nimport Actions\nimport Foundation\n\nprotocol ContextMenuConfiguration {\n    var savedContextMenu: [ActionSeed]? { get set }\n    var contextMenu: [ActionSeed] { get set }\n\n    static var availableActions: ActionSeedSections { get }\n    static var defaultContextMenu: [ActionSeed] { get }\n}\n\nextension ContextMenuConfiguration {\n    var contextMenu: [ActionSeed] {\n        get {\n            savedContextMenu ?? Self.defaultContextMenu\n        }\n        set {\n            savedContextMenu = newValue\n        }\n    }\n\n}\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/InteractionBarConfiguration.swift",
    "content": "//\n//  InteractionConfiguration.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/08/2024.\n//\n\n// swiftlint:disable line_length\n\nimport Actions\nimport Foundation\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\n\nprotocol InteractionBarConfiguration: Codable, Equatable, SwipeActionConfiguration, ContextMenuConfiguration {\n    associatedtype ActionType: ActionTypeProviding\n    associatedtype CounterType: CounterTypeProviding\n    associatedtype ReadoutType: ReadoutTypeProviding\n    \n    typealias Item = InteractionConfigurationItem<ActionType, CounterType, ReadoutType>\n    \n    var leading: [Item] { get set }\n    var trailing: [Item] { get set }\n    var readouts: [ReadoutType] { get set }\n\n    var availableWidgets: Set<Item> { get set }\n    func widgetPickerPage(_ configuration: Binding<Self>) -> SettingsPage\n    \n    /// Default configuration for this type\n    static var `default`: Self { get }\n    /// Default report configuration for this type. `nil` if inapplicable.\n    static var reportDefault: Self? { get }\n\n    static var availableActions: ActionSeedSections { get }\n    \n    init(\n        leading: [Item],\n        trailing: [Item],\n        savedSwipes: ActionSeedSwipeConfiguration?,\n        readouts: [ReadoutType],\n        availableWidgets: Set<Item>,\n        savedContextMenu: [ActionSeed]?\n    )\n}\n\nextension InteractionBarConfiguration {\n    /// Convert the `InteractionBarConfiguration` to another type of `InteractionBarConfiguration`. This is done by finding cases with\n    /// matching `rawValue` in the new type. If one cannot be found, the item is omitted.\n    func applying(other: some InteractionBarConfiguration, types: Set<InteractionBarConfigurationConversionType>) -> Self {\n        .init(\n            leading: types.contains(.bar) ? other.leading.compactMap { $0.convert() } : leading,\n            trailing: types.contains(.bar) ? other.trailing.compactMap { $0.convert() } : trailing,\n            savedSwipes: types.contains(.swipe) ? other.savedSwipes?.filter(allowed: Self.availableActions.all) : savedSwipes,\n            readouts: types.contains(.bar) ? other.readouts.compactMap { .init(rawValue: $0.rawValue) } : readouts,\n            availableWidgets: types.contains(.bar) ? .init(other.availableWidgets.compactMap { $0.convert() }) : availableWidgets,\n            savedContextMenu: types.contains(.contextMenu) ? other.savedContextMenu.map { $0.filter { Self.availableActions.all.contains($0) } } : savedContextMenu\n        )\n    }\n    \n    var all: [Item] { leading + trailing }\n    \n    func associatedReadouts(context: any InteractableProviding) -> Set<ReadoutType> {\n        all.reduce(into: Set<ReadoutType>()) { result, element in\n            result.formUnion(element.associatedReadouts(context: context))\n        }\n    }\n}\n\n// swiftlint:disable:next type_name\nenum InteractionBarConfigurationConversionType {\n    case swipe, bar, contextMenu\n}\n\nenum InteractionConfigurationItem<\n    ActionType: ActionTypeProviding,\n    CounterType: CounterTypeProviding,\n    ReadoutType: ReadoutTypeProviding\n>: Codable, Hashable {\n    case action(ActionType)\n    case counter(CounterType)\n    \n    static var allCases: [InteractionConfigurationItem] {\n        CounterType.allCases.map { .counter($0) } + ActionType.allCases.map { .action($0) }\n    }\n    \n    fileprivate func convert<\n        A: ActionTypeProviding,\n        C: CounterTypeProviding,\n        R: ReadoutTypeProviding\n    >() -> InteractionConfigurationItem<A, C, R>? {\n        switch self {\n        case let .action(action):\n            if let value = A(rawValue: action.rawValue) {\n                return .action(value)\n            } else {\n                return nil\n            }\n        case let .counter(counter):\n            if let value = C(rawValue: counter.rawValue) {\n                return .counter(value)\n            } else {\n                return nil\n            }\n        }\n    }\n    \n    // This is used to determine when an interaction bar configuration is considered \"full\"\n    var score: Int {\n        switch self {\n        case .action: 1\n        case let .counter(counter):\n            counter.appearance.leading == nil || counter.appearance.trailing == nil ? 2 : 3\n        }\n    }\n    \n    func associatedReadouts(context: any InteractableProviding) -> Set<ReadoutType> {\n        switch self {\n        case let .action(actionType):\n            guard let ret = actionType.associatedReadouts(context: context) as? Set<ReadoutType> else {\n                assertionFailure(\"Could not cast to ReadoutType\")\n                return []\n            }\n            return ret\n        case let .counter(counterType):\n            guard let ret = counterType.associatedReadouts(context: context) as? Set<ReadoutType> else {\n                assertionFailure(\"Could not cast to ReadoutType\")\n                return []\n            }\n            return ret\n        }\n    }\n}\n\nprotocol ActionTypeProviding: Codable, CaseIterable, Hashable, RawRepresentable where RawValue == String {\n    associatedtype Configuration: InteractionBarConfiguration\n    \n    var appearance: ActionAppearance { get }\n    \n    static var defaultWidgets: [Self] { get }\n    \n    func associatedReadouts(context: any InteractableProviding) -> Set<Configuration.ReadoutType>\n}\n\nprotocol CounterTypeProviding: Codable, CaseIterable, Hashable, RawRepresentable where RawValue == String {\n    associatedtype Configuration: InteractionBarConfiguration\n    \n    var appearance: CounterAppearance { get }\n    \n    static var defaultWidgets: [Self] { get }\n    \n    func associatedReadouts(context: any InteractableProviding) -> Set<Configuration.ReadoutType>\n}\n\nprotocol ReadoutTypeProviding: Codable, CaseIterable, Hashable, RawRepresentable where RawValue == String {\n    var appearance: MockReadoutAppearance { get }\n    \n    func compatibleWith(otherReadouts: Set<Self>) -> Bool\n}\n\nstruct InteractionBarConfigurations: Codable {\n    var post: PostBarConfiguration\n    var comment: CommentBarConfiguration\n    var reply: ReplyBarConfiguration\n    var postReport: PostBarConfiguration\n    var commentReport: CommentBarConfiguration\n    \n    static var `default`: Self {\n        .init(\n            post: .default,\n            comment: .default,\n            reply: .default,\n            postReport: .reportDefault_,\n            commentReport: .reportDefault_\n        )\n    }\n    \n    init(\n        post: PostBarConfiguration,\n        comment: CommentBarConfiguration,\n        reply: ReplyBarConfiguration,\n        postReport: PostBarConfiguration,\n        commentReport: CommentBarConfiguration\n    ) {\n        self.post = post\n        self.comment = comment\n        self.reply = reply\n        self.postReport = postReport\n        self.commentReport = commentReport\n    }\n    \n    init(from decoder: any Decoder) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        self.post = try container.decodeIfPresent(PostBarConfiguration.self, forKey: .post) ?? .default\n        self.comment = try container.decodeIfPresent(CommentBarConfiguration.self, forKey: .comment) ?? .default\n        self.reply = try container.decodeIfPresent(ReplyBarConfiguration.self, forKey: .reply) ?? .default\n        self.postReport = try container.decodeIfPresent(PostBarConfiguration.self, forKey: .postReport) ?? .reportDefault_\n        self.commentReport = try container.decodeIfPresent(CommentBarConfiguration.self, forKey: .commentReport) ?? .reportDefault_\n    }\n}\n\nstruct MockReadoutAppearance {\n    let icon: Icon\n    let label: String\n}\n\n// swiftlint:enable line_length\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/PostBarConfiguration+Types.swift",
    "content": "//\n//  PostBarConfiguration+Types.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-15.\n//\n\nimport Actions\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nextension PostBarConfiguration {\n    enum ActionType: String, ActionTypeProviding {\n        typealias Configuration = PostBarConfiguration // swiftlint:disable:this nesting\n        \n        case upvote\n        case downvote\n        case save\n        case reply\n        case share\n        case selectText\n        case hide\n        case block\n        case report\n        case crossPost\n        case lock\n        case pin\n        case resolve\n        case remove\n        case ban\n        \n        static var defaultWidgets: [ActionType] { [\n            .upvote,\n            .downvote,\n            .save,\n            .reply,\n            .share\n        ] }\n        \n        static var defaultReportWidgets: [ActionType] { [\n            .share,\n            .lock,\n            .pin,\n            .resolve,\n            .remove,\n            .ban\n        ] }\n        \n        var appearance: ActionAppearance {\n            switch self {\n            case .upvote: .upvote(isOn: false)\n            case .downvote: .downvote(isOn: false)\n            case .save: .save(isOn: false)\n            case .reply: .reply()\n            case .share: .share()\n            case .selectText: .selectText()\n            case .hide: .hide(isOn: false)\n            case .block: .block(isOn: false)\n            case .report: .report()\n            case .crossPost: .crossPost()\n            case .lock: .lock(isOn: false)\n            case .pin: .pin(isOn: false)\n            case .resolve: .resolve(isOn: false)\n            case .remove: .remove(isOn: false)\n            case .ban: .banFromCommunity(isOn: false)\n            }\n        }\n        \n        func associatedReadouts(context: any InteractableProviding) -> Set<PostBarConfiguration.ReadoutType> {\n            switch self {\n            case .upvote: context.votes.value?.myVote ?? .none == .upvote ? [.upvote, .score] : [.upvote]\n            case .downvote: context.votes.value?.myVote ?? .none == .downvote ? [.downvote, .score] : [.downvote]\n            case .save: [.saved]\n            case .reply, .share, .selectText, .hide, .block, .report, .crossPost, .lock, .pin, .resolve, .remove, .ban: []\n            }\n        }\n        \n        func associatedReadouts(context: Post) -> Set<PostBarConfiguration.ReadoutType> {\n            switch self {\n            case .upvote: context.votes.value?.myVote ?? .none == .upvote ? [.upvote, .score] : [.upvote]\n            case .downvote: context.votes.value?.myVote ?? .none == .downvote ? [.downvote, .score] : [.downvote]\n            case .save: [.saved]\n            case .reply, .share, .selectText, .hide, .block, .report, .crossPost, .lock, .pin, .resolve, .remove, .ban: []\n            }\n        }\n        \n        var actionSeed: ActionSeed {\n            switch self {\n            case .upvote: .upvote\n            case .downvote: .downvote\n            case .save: .save\n            case .reply: .reply\n            case .share: .share\n            case .selectText: .selectText\n            case .hide: .hide\n            case .block: .blockCreator\n            case .report: .report\n            case .crossPost: .crosspost\n            case .lock: .lock\n            case .pin: .pin\n            case .resolve: .resolveReport\n            case .remove: .remove\n            case .ban: .banCreator\n            }\n        }\n    }\n    \n    enum CounterType: String, CounterTypeProviding {\n        typealias Configuration = PostBarConfiguration // swiftlint:disable:this nesting\n        \n        case score\n        case upvote\n        case downvote\n        case reply\n        \n        static var defaultWidgets: [CounterType] { allCases }\n        \n        var appearance: CounterAppearance {\n            switch self {\n            case .score: .score()\n            case .upvote: .upvote()\n            case .downvote: .downvote()\n            case .reply: .reply()\n            }\n        }\n        \n        func associatedReadouts(context: any InteractableProviding) -> Set<PostBarConfiguration.ReadoutType> {\n            switch self {\n            case .score: [.upvote, .downvote, .score]\n            case .upvote: context.votes.value?.myVote ?? .none == .upvote ? [.upvote, .score] : [.upvote]\n            case .downvote: context.votes.value?.myVote ?? .none == .downvote ? [.downvote, .score] : [.downvote]\n            case .reply: []\n            }\n        }\n        \n        func associatedReadouts(context: Post) -> Set<PostBarConfiguration.ReadoutType> {\n            switch self {\n            case .score: [.upvote, .downvote, .score]\n            case .upvote: context.votes.value?.myVote ?? .none == .upvote ? [.upvote, .score] : [.upvote]\n            case .downvote: context.votes.value?.myVote ?? .none == .downvote ? [.downvote, .score] : [.downvote]\n            case .reply: []\n            }\n        }\n    }\n    \n    enum ReadoutType: String, ReadoutTypeProviding {\n        case created\n        case score\n        case upvote\n        case downvote\n        case comment\n        case saved\n        \n        var appearance: MockReadoutAppearance {\n            switch self {\n            case .created: .init(icon: .general.time, label: \"18h\")\n            case .score: .init(icon: .lemmy.votes, label: \"7\")\n            case .upvote: .init(icon: .lemmy.upvoted, label: \"9\")\n            case .downvote: .init(icon: .lemmy.downvoted, label: \"2\")\n            case .comment: .init(icon: .lemmy.replies, label: \"1\")\n            case .saved: .init(icon: .lemmy.saved, label: \"\")\n            }\n        }\n        \n        func compatibleWith(otherReadouts: Set<Self>) -> Bool {\n            switch self {\n            case .score: otherReadouts.isDisjoint(with: [.upvote, .downvote])\n            case .upvote, .downvote: !otherReadouts.contains(.score)\n            default: true\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/PostBarConfiguration.swift",
    "content": "//\n//  PostInteraction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 14/06/2024.\n//\n\nimport Actions\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nstruct PostBarConfiguration: InteractionBarConfiguration, SwipeActionConfiguration {\n    var leading: [Item]\n    var trailing: [Item]\n    var readouts: [ReadoutType]\n    var savedContextMenu: [ActionSeed]?\n\n    var savedSwipes: ActionSeedSwipeConfiguration?\n\n    static var defaultSwipes: ActionSeedSwipeConfiguration {\n        .init(leading: [.downvote, .upvote], trailing: [.save, .reply])\n    }\n\n    static var defaultContextMenu: [ActionSeed] {\n        [.selectText, .share, .blockCreator, .report, .edit, .delete, .remove, .banCreator, .resolveReport]\n    }\n\n    var availableWidgets: Set<Item>\n    func widgetPickerPage(_ configuration: Binding<Self>) -> SettingsPage { .postBarWidgetPicker(configuration) }\n    \n    init(\n        leading: [Item],\n        trailing: [Item],\n        savedSwipes: ActionSeedSwipeConfiguration?,\n        readouts: [ReadoutType],\n        availableWidgets: Set<Item>,\n        savedContextMenu: [ActionSeed]?\n    ) {\n        self.leading = leading\n        self.trailing = trailing\n        self.savedSwipes = savedSwipes\n        self.readouts = readouts\n        self.availableWidgets = availableWidgets\n        self.savedContextMenu = savedContextMenu\n    }\n    \n    init(from decoder: any Decoder) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        self.leading = try container.decodeIfPresent([Item].self, forKey: .leading) ?? [.counter(.score)]\n        self.trailing = try container.decodeIfPresent([Item].self, forKey: .trailing) ?? [.action(.save), .action(.reply)]\n        self.readouts = try container.decodeIfPresent([ReadoutType].self, forKey: .readouts) ?? [.created, .comment]\n\n        self.availableWidgets = try container.decodeIfPresent(Set<Item>.self, forKey: .availableWidgets) ??\n            .init(CounterType.defaultWidgets.map { .counter($0) } + ActionType.defaultWidgets.map { .action($0) })\n\n        if let contextMenuKeys = try container.decodeIfPresent([String].self, forKey: .savedContextMenu) {\n            let allActions = Self.availableActions.all\n            self.savedContextMenu = contextMenuKeys.compactMap { key in allActions.first(where: {$0.key == key}) }\n        } else {\n            self.savedContextMenu = nil\n        }\n\n        let swipeConfigurationContainer = try? container.nestedContainer(\n            keyedBy: ActionSeedSwipeConfiguration.CodingKeys.self,\n            forKey: .swipes\n        )\n        if let swipeConfigurationContainer {\n            self.savedSwipes = try .init(from: swipeConfigurationContainer, availableActions: Self.availableActions.all)\n        } else {\n            // Convert from Mlem 2.4 -> 2.5 format\n            let leadingSwipes = try container.decodeIfPresent([ActionType].self, forKey: .leadingSwipes) ?? [.upvote, .downvote]\n            let trailingSwipes = try container.decodeIfPresent([ActionType].self, forKey: .trailingSwipes) ?? [.save, .reply]\n\n            let swipes = ActionSeedSwipeConfiguration(\n                leading: leadingSwipes.map(\\.actionSeed),\n                trailing: trailingSwipes.map(\\.actionSeed)\n            )\n\n            if swipes == Self.defaultSwipes {\n                self.savedSwipes = nil\n            } else {\n                self.savedSwipes = swipes\n            }\n        }\n    }\n\n    enum CodingKeys: CodingKey {\n        case leading\n        case trailing\n        case readouts \n        case availableWidgets\n        case savedContextMenu\n        case swipes\n\n        // Used for conversion from Mlem 2.4 -> 2.5 format\n        case leadingSwipes\n        case trailingSwipes\n    }\n\n    func encode(to encoder: any Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(self.leading, forKey: .leading)\n        try container.encode(self.trailing, forKey: .trailing)\n        try container.encode(self.readouts, forKey: .readouts)\n        try container.encode(self.availableWidgets, forKey: .availableWidgets)\n        try container.encode(self.savedContextMenu, forKey: .savedContextMenu)\n        try container.encode(self.savedSwipes, forKey: .swipes)\n    }\n    \n    static var `default`: Self {\n        .init(\n            leading: [.counter(.score)],\n            trailing: [.action(.save), .action(.reply)],\n            savedSwipes: nil,\n            readouts: [.created, .comment],\n            availableWidgets: .init(CounterType.defaultWidgets.map { .counter($0) } + ActionType.defaultWidgets.map { .action($0) }),\n            savedContextMenu: nil\n        )\n    }\n    \n    static var reportDefault_: Self {\n        .init(\n            leading: [.action(.resolve), .action(.lock)],\n            trailing: [.action(.ban), .action(.remove)],\n            savedSwipes: nil,\n            readouts: [.upvote, .downvote, .created, .comment],\n            availableWidgets: .init(ActionType.defaultReportWidgets.map { .action($0) }),\n            savedContextMenu: nil\n        )\n    }\n\n    static var availableActions: ActionSeedSections { .init(sections: [\n            [\n                .upvote,\n                .downvote,\n                .save,\n                .reply,\n                .selectText,\n                .share,\n                .crosspost,\n                .hide,\n                .createImage,\n                .report,\n                .edit,\n                .delete\n            ],\n            [\n                .blockCreator,\n                .copyAuthorName,\n                .openCreatorModlog,\n                .sendCreatorMessage\n            ],\n            [\n                .pin,\n                .lock,\n                .markNsfw,\n                .viewVotes,\n                .remove,\n                .banCreator,\n                .purge,\n                .purgeCreator,\n                .resolveReport\n            ]\n        ])\n    }\n    \n    static var reportDefault: Self? { .reportDefault_ }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/ReplyBarConfiguration+Types.swift",
    "content": "//\n//  ReplyBarConfiguration+Types.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-15.\n//\n\nimport Actions\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nextension ReplyBarConfiguration {\n    enum ActionType: String, ActionTypeProviding {\n        typealias Configuration = ReplyBarConfiguration // swiftlint:disable:this nesting\n        \n        case upvote\n        case downvote\n        case save\n        case reply\n        case markRead\n        case selectText\n        case report\n        \n        static var defaultWidgets: [ActionType] { [\n            .upvote,\n            .downvote,\n            .save,\n            .reply,\n            .markRead\n        ] }\n        \n        var appearance: ActionAppearance {\n            switch self {\n            case .upvote: .upvote(isOn: false)\n            case .downvote: .downvote(isOn: false)\n            case .save: .save(isOn: false)\n            case .reply: .reply()\n            case .markRead: .markRead(isOn: false)\n            case .selectText: .selectText()\n            case .report: .report()\n            }\n        }\n        \n        func associatedReadouts(context: any InteractableProviding) -> Set<ReplyBarConfiguration.ReadoutType> {\n            switch self {\n            case .upvote: context.votes.value?.myVote ?? .none == .upvote ? [.upvote, .score] : [.upvote]\n            case .downvote: context.votes.value?.myVote ?? .none == .downvote ? [.downvote, .score] : [.downvote]\n            case .save: [.saved]\n            case .reply, .markRead, .selectText, .report: []\n            }\n        }\n\n        var actionSeed: ActionSeed {\n            switch self {\n            case .upvote: .upvote\n            case .downvote: .downvote\n            case .save: .save\n            case .reply: .reply\n            case .markRead: .markRead\n            case .selectText: .selectText\n            case .report: .report\n            }\n        }\n    }\n    \n    enum CounterType: String, CounterTypeProviding {\n        typealias Configuration = ReplyBarConfiguration // swiftlint:disable:this nesting\n        \n        case score\n        case upvote\n        case downvote\n        case reply\n        \n        static var defaultWidgets: [CounterType] { allCases }\n        \n        var appearance: CounterAppearance {\n            switch self {\n            case .score: .score()\n            case .upvote: .upvote()\n            case .downvote: .downvote()\n            case .reply: .reply()\n            }\n        }\n        \n        func associatedReadouts(context: any InteractableProviding) -> Set<ReplyBarConfiguration.ReadoutType> {\n            switch self {\n            case .score: [.upvote, .downvote, .score]\n            case .upvote: context.votes.value?.myVote ?? .none == .upvote ? [.upvote, .score] : [.upvote]\n            case .downvote: context.votes.value?.myVote ?? .none == .downvote ? [.downvote, .score] : [.downvote]\n            case .reply: []\n            }\n        }\n    }\n    \n    enum ReadoutType: String, ReadoutTypeProviding {\n        case created\n        case score\n        case upvote\n        case downvote\n        case comment\n        case saved\n        \n        var appearance: MockReadoutAppearance {\n            switch self {\n            case .created: .init(icon: .general.time, label: \"18h\")\n            case .score: .init(icon: .lemmy.votes, label: \"7\")\n            case .upvote: .init(icon: .lemmy.upvoted, label: \"9\")\n            case .downvote: .init(icon: .lemmy.downvoted, label: \"2\")\n            case .comment: .init(icon: .lemmy.replies, label: \"1\")\n            case .saved: .init(icon: .lemmy.saved, label: \"\")\n            }\n        }\n        \n        func compatibleWith(otherReadouts: Set<Self>) -> Bool {\n            switch self {\n            case .score: otherReadouts.isDisjoint(with: [.upvote, .downvote])\n            case .upvote, .downvote: !otherReadouts.contains(.score)\n            default: true\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/ReplyBarConfiguration.swift",
    "content": "//\n//  InboxInteraction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 14/06/2024.\n//\n\nimport Actions\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ReplyBarConfiguration: InteractionBarConfiguration, SwipeActionConfiguration {\n    var leading: [Item]\n    var trailing: [Item]\n    var readouts: [ReadoutType]\n    var savedContextMenu: [ActionSeed]?\n\n    var savedSwipes: ActionSeedSwipeConfiguration?\n\n    static var defaultSwipes: ActionSeedSwipeConfiguration {\n        .init(leading: [.downvote, .upvote], trailing: [.save, .reply])\n    }\n\n    static var defaultContextMenu: [ActionSeed] {\n        [.markRead, .share, .blockCreator, .report]\n    }\n\n    var availableWidgets: Set<Item>\n    func widgetPickerPage(_ configuration: Binding<Self>) -> SettingsPage { .replyBarWidgetPicker(configuration) }\n    \n    init(\n        leading: [Item],\n        trailing: [Item],\n        savedSwipes: ActionSeedSwipeConfiguration?,\n        readouts: [ReadoutType],\n        availableWidgets: Set<Item>,\n        savedContextMenu: [ActionSeed]?\n    ) {\n        self.leading = leading\n        self.trailing = trailing\n        self.savedSwipes = savedSwipes\n        self.readouts = readouts\n        self.availableWidgets = availableWidgets\n        self.savedContextMenu = savedContextMenu\n    }\n\n    init(from decoder: any Decoder) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        self.leading = try container.decodeIfPresent([Item].self, forKey: .leading) ?? [.counter(.score)]\n        self.trailing = try container.decodeIfPresent([Item].self, forKey: .trailing) ?? [.action(.save), .action(.reply)]\n        self.readouts = try container.decodeIfPresent([ReadoutType].self, forKey: .readouts) ?? [.created, .comment]\n        self.availableWidgets = try container.decodeIfPresent(Set<Item>.self, forKey: .availableWidgets) ??\n            .init(CounterType.defaultWidgets.map { .counter($0) } + ActionType.defaultWidgets.map { .action($0) })\n\n        if let contextMenuKeys = try container.decodeIfPresent([String].self, forKey: .savedContextMenu) {\n            let allActions = Self.availableActions.all\n            self.savedContextMenu = contextMenuKeys.compactMap { key in allActions.first(where: {$0.key == key}) }\n        } else {\n            self.savedContextMenu = nil\n        }\n\n        let swipeConfigurationContainer = try? container.nestedContainer(\n            keyedBy: ActionSeedSwipeConfiguration.CodingKeys.self,\n            forKey: .swipes\n        )\n        if let swipeConfigurationContainer {\n            self.savedSwipes = try .init(from: swipeConfigurationContainer, availableActions: Self.availableActions.all)\n        } else {\n            // Convert from Mlem 2.4 -> 2.5 format\n            let leadingSwipes = try container.decodeIfPresent([ActionType].self, forKey: .leadingSwipes) ?? [.upvote, .downvote]\n            let trailingSwipes = try container.decodeIfPresent([ActionType].self, forKey: .trailingSwipes) ?? [.save, .reply]\n\n            let swipes = ActionSeedSwipeConfiguration(\n                leading: leadingSwipes.map(\\.actionSeed),\n                trailing: trailingSwipes.map(\\.actionSeed)\n            )\n\n            if swipes == Self.defaultSwipes {\n                self.savedSwipes = nil\n            } else {\n                self.savedSwipes = swipes\n            }\n        }\n    }\n\n    enum CodingKeys: CodingKey {\n        case leading\n        case trailing\n        case readouts \n        case availableWidgets\n        case savedContextMenu\n        case swipes\n\n        // Used for conversion from Mlem 2.4 -> 2.5 format\n        case leadingSwipes\n        case trailingSwipes\n    }\n\n    func encode(to encoder: any Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(self.leading, forKey: .leading)\n        try container.encode(self.trailing, forKey: .trailing)\n        try container.encode(self.readouts, forKey: .readouts)\n        try container.encode(self.availableWidgets, forKey: .availableWidgets)\n        try container.encode(self.savedContextMenu, forKey: .savedContextMenu)\n        try container.encode(self.savedSwipes, forKey: .swipes)\n    }\n\n    static var `default`: Self {\n        .init(\n            leading: [.counter(.score)],\n            trailing: [.action(.save), .action(.reply)],\n            savedSwipes: nil,\n            readouts: [.created, .comment],\n            availableWidgets: .init(CounterType.defaultWidgets.map { .counter($0) } + ActionType.defaultWidgets.map { .action($0) }),\n            savedContextMenu: nil\n        )\n    }\n\n    static var availableActions: ActionSeedSections { .init(sections: [\n            [\n                .upvote,\n                .downvote,\n                .save,\n                .reply,\n                .markRead,\n                .selectText,\n                .share,\n                .createImage,\n                .report,\n                .edit,\n                .delete\n            ],\n            [\n                .blockCreator,\n                .copyAuthorName,\n                .openCreatorModlog,\n                .sendCreatorMessage\n            ],\n            [\n                .viewVotes,\n                .remove,\n                .banCreator,\n                .purge,\n                .purgeCreator,\n                .resolveReport\n            ]\n        ])\n    }\n\n    static var reportDefault: Self? { nil }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/Interaction/SwipeActionConfiguration.swift",
    "content": "//\n//  SwipeActionConfiguration.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-15.\n//\n\nimport Actions\nimport Foundation\n\nprotocol SwipeActionConfiguration {\n    var savedSwipes: ActionSeedSwipeConfiguration? { get set }\n\n    static var availableActions: ActionSeedSections { get }\n    static var defaultSwipes: ActionSeedSwipeConfiguration { get }\n}\n\nextension SwipeActionConfiguration {\n    var swipes: ActionSeedSwipeConfiguration {\n        get {\n            savedSwipes ?? Self.defaultSwipes\n        }\n        set {\n            savedSwipes = newValue\n        }\n    }\n\n}\n"
  },
  {
    "path": "Mlem/App/Enums/MlemError.swift",
    "content": "//\n//  MlemError.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-02-06.\n//\n\nenum MlemError: Error {\n    case modelError(String)\n    case navigationError(String)\n    case unexpectedValue\n    case cannotAccessSecurityScopedResource\n    case mediaError(String)\n}\n\nextension MlemError: CustomStringConvertible {\n    public var description: String {\n        switch self {\n        case let .modelError(string):\n            return \"Model Error: \\(string)\"\n        case let .navigationError(string):\n            return \"Navigation Error: \\(string)\"\n        case .cannotAccessSecurityScopedResource:\n            return \"Cannot access security-scoped resource\"\n        case .unexpectedValue:\n            return \"Encountered unexpected value\"\n        case let .mediaError(string):\n            return \"Media Error: \\(string)\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/NsfwBlurBehavior.swift",
    "content": "//\n//  NsfwBlurBehavior.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-22.\n//\n\nimport Icons\nimport Foundation\n\nenum NsfwBlurBehavior: String, CaseIterable, Codable {\n    case always, outsideCommunity, never\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .always: \"Always\"\n        case .outsideCommunity: \"Outside NSFW Communities\"\n        case .never: \"Never\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .always: .general.success\n        case .outsideCommunity: .lemmy.community\n        case .never: .general.failure\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/PersonFlair.swift",
    "content": "//\n//  UserFlair.swift\n//  Mlem\n//\n//  Created by Sjmarf on 07/10/2023.\n//\n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nenum PersonFlair: Hashable {\n    case admin\n    case moderator\n    case developer\n    case bot\n    case op\n    case cakeDay\n    case bannedFromInstance\n    case bannedFromCommunity\n    case accountAge(Date)\n    \n    // this defines the order in which flairs appear\n    var sortVal: Int {\n        switch self {\n        case .admin: 0\n        case .moderator: 1\n        case .developer: 2\n        case .bot: 3\n        case .op: 4\n        case .cakeDay: 5\n        case .bannedFromInstance: 6\n        case .bannedFromCommunity: 7\n        case .accountAge: 8\n        }\n    }\n    \n    var text: String {\n        switch self {\n        case let .accountAge(created):\n            let components = Calendar.current.dateComponents(\n                [.year, .month, .day, .hour, .minute, .second],\n                from: created,\n                to: .now\n            ).roundingDownToMostSignificantComponent()\n            \n            let formatter = DateComponentsFormatter()\n            formatter.unitsStyle = .abbreviated\n            formatter.maximumUnitCount = 1\n            formatter.allowedUnits = [.year, .month, .day, .hour, .minute, .second]\n            return formatter.string(from: components) ?? \"\"\n        default:\n            return \"\"\n        }\n    }\n    \n    var color: ThemedColor {\n        switch self {\n        case .admin: .themedAdministration\n        case .moderator: .themedModeration\n        case .op: .themedColorfulAccent(0)\n        case .bot: .themedColorfulAccent(5)\n        case .bannedFromInstance, .bannedFromCommunity: .themedNegative\n        case .developer: .themedColorfulAccent(4)\n        case .cakeDay: .themedColorfulAccent(1)\n        case let .accountAge(date): AccountAgeBracket(date: date).color\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .admin: .lemmy.administration\n        case .moderator: .lemmy.moderation\n        case .op: .lemmy.opFlair\n        case .bot: .lemmy.botFlair\n        case .bannedFromInstance: .lemmy.bannedFromInstance\n        case .bannedFromCommunity: .lemmy.bannedFromCommunity\n        case .developer: .lemmy.developerFlair\n        case .cakeDay: .lemmy.cakeDay\n        case let .accountAge(date): AccountAgeBracket(date: date).icon\n        }\n    }\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .admin: \"Administrator\"\n        case .bot: \"Bot Account\"\n        case .bannedFromInstance: \"Banned from Instance\"\n        case .bannedFromCommunity: \"Banned from Community\"\n        case .moderator: \"Moderator\"\n        case .developer: \"Mlem Developer\"\n        case .op: \"Original Poster\"\n        case .cakeDay: \"Cake Day\"\n        case let .accountAge(date): \"Account Created \\(date.formatted(date: .abbreviated, time: .omitted))\"\n        }\n    }\n    \n    var textView: Text {\n        (Text(Image(icon: icon)) + Text(text).fontWeight(.semibold))\n            .foregroundStyle(color)\n    }\n}\n\nprivate enum AccountAgeBracket: CaseIterable {\n    case upToOneMonth\n    case upToOneYear\n    case upToTwoYears // In future we could increase this to three years?\n    case other\n    case beforeInflux\n    \n    init(date: Date) {\n        if date < Date(timeIntervalSince1970: 1_685_617_200) { // 2023-06-01\n            self = .beforeInflux\n            return\n        }\n        \n        let intervalSinceCreation = Date.now.timeIntervalSince(date)\n        let day: TimeInterval = 24 * 60 * 60\n\n        if intervalSinceCreation < 30 * day {\n            self = .upToOneMonth\n        } else if intervalSinceCreation < 365 * day {\n            self = .upToOneYear\n        } else if intervalSinceCreation < 2 * 365 * day {\n            self = .upToTwoYears\n        } else {\n            self = .other\n        }\n    }\n    \n    var icon: Icon { .lemmy.accountAgeFlair(bracket: self) }\n    \n    var color: ThemedColor {\n        .themedAccountAgeColor(Self.allCases.firstIndex(of: self)!)\n    }\n}\n\nextension [PersonFlair] {\n    var textView: Text {\n        if isEmpty {\n            Text(verbatim: \"\")\n        } else {\n            reduce(Text(verbatim: \"\")) { $0 + $1.textView } + Text(verbatim: \" \")\n        }\n    }\n}\n\nprivate extension Icon.LemmyIcons {\n    func accountAgeFlair(bracket: AccountAgeBracket) -> Icon {\n        switch bracket {\n        case .upToOneMonth: .lemmy.newAccountFlair\n        case .upToOneYear: .init(\"camera.macro\")\n        case .upToTwoYears: .init(\"tree.fill\")\n        case .other: .init(\"mountain.2.fill\")\n        case .beforeInflux: .init(\"fossil.shell.fill\")\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/PostViewLinkType.swift",
    "content": "//\n//  PostViewLinkType.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-17.\n//\n\nimport Foundation\n\nenum PostViewNavigationLink {\n    case creator, community\n}\n"
  },
  {
    "path": "Mlem/App/Enums/ReadPostIndicator.swift",
    "content": "//\n//  ReadPostIndicator.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-12-25.\n//\n\nimport Foundation\n\nenum ReadPostIndicator: String, CaseIterable, Codable {\n    case outline, checkmark, none\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .outline: \"Outline\"\n        case .checkmark: \"Checkmark\"\n        case .none: \"None\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/TabBarLongPressAction.swift",
    "content": "//\n//  TabBarLongPressAction.swift\n//  Mlem\n//\n// Created by Bedir Ekim on 21.05.2025.\n//\n\nimport Foundation\nimport Icons\n\nenum TabBarLongPressAction: String, CaseIterable, Codable {\n    case openAccountSwitcher, switchToMostRecentAccount\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .openAccountSwitcher: \"Open Account Switcher\"\n        case .switchToMostRecentAccount: \"Switch to Most Recent Account\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .openAccountSwitcher: .lemmy.openAccountSwitcher\n        case .switchToMostRecentAccount: .lemmy.switchAccount\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Enums/ZoomSliderLocation.swift",
    "content": "//\n//  ZoomSliderLocation.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-02-02.\n//\n\nimport Foundation\nimport Icons\n\nenum ZoomSliderLocation: String, CaseIterable, Codable {\n    case left, right, either, none\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .left: \"Left\"\n        case .right: \"Right\"\n        case .either: \"Either\"\n        case .none: \"Disabled\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .left: .general.backward\n        case .right: .general.forward\n        case .either: .settings.leftRight\n        case .none: .general.circle\n        }\n    }\n    \n    var leftEnabled: Bool {\n        switch self {\n        case .left, .either: true\n        default: false\n        }\n    }\n    \n    var rightEnabled: Bool {\n        switch self {\n        case .right, .either: true\n        default: false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Globals/Definitions/AccountsTracker.swift",
    "content": "//\n//  AccountsTracker.swift\n//  Mlem\n//\n//  Created by David Bureš on 05.05.2023.\n//\n\nimport Combine\nimport Dependencies\nimport Foundation\nimport MlemMiddleware\nimport Observation\n\nprivate let defaultInstanceGroupKey = \"Other\"\n\n@Observable\nclass AccountsTracker {\n    enum SaveType {\n        case user, guest, all\n    }\n    \n    static let main: AccountsTracker = .init()\n    \n    @ObservationIgnored @Dependency(\\.persistenceRepository) private var persistenceRepository\n    \n    var userAccounts: [UserAccount] = .init()\n    var guestAccounts: [GuestAccount] = .init()\n    \n    var allAccounts: [any Account] { userAccounts + guestAccounts }\n    \n    // Used on startup to determine which account should be made active\n    func mostRecentAccount() -> any Account {\n        let allAccounts: [any Account] = userAccounts + guestAccounts\n        if let activeAccount = allAccounts.first(where: { $0.activityState == .active }) {\n            return activeAccount\n        }\n        let sorted = allAccounts.sorted(by: { $0.activityState.lastUsed ?? .distantPast < $1.activityState.lastUsed ?? .distantPast })\n        if let lastUsedAccount = sorted.last {\n            return lastUsedAccount\n        }\n        return userAccounts.first ?? defaultGuestAccount\n    }\n    \n    var defaultGuestAccount: GuestAccount {\n        // This will never fail because we're passing a literal URL that is known to always succeed\n        // swiftlint:disable:next force_try\n        try! GuestAccount.getGuestAccount(url: URL(string: \"https://lemmy.world/\")!)\n    }\n    \n    var isEmpty: Bool { userAccounts.isEmpty && guestAccounts.isEmpty }\n    \n    private var cancellables = Set<AnyCancellable>()\n    \n    private init() {\n        self.userAccounts = persistenceRepository.loadUserAccounts()\n        self.guestAccounts = persistenceRepository.loadGuestAccounts()\n    }\n    \n    func addAccount(account: any Account) {\n        if let account = account as? UserAccount {\n            guard !userAccounts.contains(where: { $0 === account }) else {\n                assertionFailure(\"Tried to add a duplicate account to the tracker\")\n                return\n            }\n            userAccounts.append(account)\n            saveAccounts(ofType: .user)\n        } else if let account = account as? GuestAccount {\n            guard !guestAccounts.contains(where: { $0 === account }) else {\n                assertionFailure(\"Tried to add a duplicate account to the tracker\")\n                return\n            }\n            guestAccounts.append(account)\n            saveAccounts(ofType: .guest)\n        } else {\n            assertionFailure()\n        }\n    }\n    \n    func removeAccount(account: any Account) {\n        if let account = account as? UserAccount {\n            guard let index = userAccounts.firstIndex(where: { $0 === account }) else {\n                assertionFailure(\"Tried to remove an account that does not exist\")\n                return\n            }\n            userAccounts.remove(at: index)\n            saveAccounts(ofType: .user)\n            account.deleteTokenFromKeychain()\n        } else if let account = account as? GuestAccount {\n            guard let index = guestAccounts.firstIndex(where: { $0 === account }) else {\n                assertionFailure(\"Tried to remove an account that does not exist\")\n                return\n            }\n            guestAccounts.remove(at: index)\n            account.resetStoredSettings(withSave: false)\n            saveAccounts(ofType: .guest)\n        } else {\n            assertionFailure()\n        }\n        AppState.main.deactivate(account: account)\n        do {\n            try PersistenceRepository.liveValue.deleteAccountSettings(for: account)\n            try PersistenceRepository.liveValue.deleteVisitHistory(for: account)\n        } catch {\n            handleError(error, silent: true)\n        }\n        GuestAccountCache.main.clean()\n    }\n    \n    @discardableResult\n    func logIn(\n        client unauthenticatedApi: ApiClient,\n        usernameOrEmail: String,\n        password: String,\n        totpToken: String? = nil\n    ) async throws -> UserAccount {\n        let token = try await unauthenticatedApi.getAccountToken(\n            usernameOrEmail: usernameOrEmail,\n            password: password,\n            totpToken: totpToken\n        )\n        let username = try await unauthenticatedApi.getUsernameFromToken(token: token)\n        \n        return try await logIn(\n            username: username,\n            url: unauthenticatedApi.baseUrl,\n            token: token\n        )\n    }\n    \n    @discardableResult\n    func logIn(\n        username: String,\n        url: URL,\n        token: String\n    ) async throws -> UserAccount {\n        let authenticatedApiClient = ApiClient.getApiClient(url: url, username: username)\n        authenticatedApiClient.updateToken(token)\n        \n        // Check if account exists already\n        if let account = userAccounts.first(where: {\n            $0.name.caseInsensitiveCompare(username) == .orderedSame && $0.api.baseUrl == url\n        }) {\n            account.updateToken(token)\n            saveAccounts(ofType: .user)\n            return account\n        } else {\n            let response = try await authenticatedApiClient.getMyPerson()\n            guard let person = response.person else {\n                throw ApiClientError.unsuccessful\n            }\n            let software = try await authenticatedApiClient.software\n            let account = UserAccount(person: person, siteSoftware: software)\n            addAccount(account: account)\n            return account\n        }\n    }\n    \n    func saveAccounts(ofType type: SaveType) {\n        Task {\n            if type != .guest {\n                try await self.persistenceRepository.saveUserAccounts(userAccounts)\n            }\n            if type != .user {\n                try await self.persistenceRepository.saveGuestAccounts(guestAccounts)\n            }\n        }\n    }\n    \n    var highestLevelAccountType: AccountType {\n        userAccounts.lazy.map(\\.accountType).max() ?? .guest\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Globals/Definitions/AppState+transition.swift",
    "content": "//\n//  AppState+Transition.swift\n//  Mlem\n//\n//  Created by Sjmarf on 05/06/2024.\n//\n\nimport SwiftUI\n\nextension AppState {\n    func transition(_ account: any Account) {\n        Task { @MainActor in\n            // Close all sheets\n            NavigationModel.main.clear()\n            \n            let transition = TransitionView(account: account)\n            guard let transitionView = UIHostingController(rootView: transition).view,\n                  let window = UIApplication.shared.firstKeyWindow else {\n                return\n            }\n            \n            transitionView.overrideUserInterfaceStyle = Settings.get(\\.appearance_interfaceStyle)\n            transitionView.alpha = 0\n            window.addSubview(transitionView)\n            UIView.animate(withDuration: 0.15) {\n                transitionView.alpha = 1\n            }\n            \n            transitionView.translatesAutoresizingMaskIntoConstraints = false\n            transitionView.heightAnchor.constraint(equalTo: window.heightAnchor).isActive = true\n            transitionView.widthAnchor.constraint(equalTo: window.widthAnchor).isActive = true\n                    \n            DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {\n                UIView.animate(withDuration: 0.3) {\n                    transitionView.alpha = 0\n                } completion: { _ in\n                    transitionView.removeFromSuperview()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Globals/Definitions/AppState.swift",
    "content": "//\n//  AppState.swift\n//  Mlem\n//\n//  Created by Sjmarf on 17/02/2024.\n//\n\nimport Dependencies\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\n@Observable\nclass AppState {\n    @ObservationIgnored @Namespace var namespace\n\n    private(set) var guestSession: GuestSession! {\n        didSet {\n            if oldValue != guestSession {\n                oldValue?.deactivate()\n            }\n        }\n    }\n    \n    private(set) var activeSessions: [UserSession] = [] {\n        didSet {\n            if oldValue != activeSessions {\n                for session in Set(oldValue).subtracting(activeSessions) {\n                    session.deactivate()\n                }\n            }\n        }\n    }\n    \n    var contentViewTab: ContentView.Tab = .feeds\n    \n    /// ``ContentView`` watches this for changes. When it is toggled, the app is refreshed.\n    var appRefreshToggle: Bool = true\n    \n    private init() {\n        self.guestSession = .init(account: AccountsTracker.main.defaultGuestAccount)\n        setAccount(to: AccountsTracker.main.mostRecentAccount())\n    }\n  \n    // TODO: updated mocks\n//    #if DEBUG\n//        private init(api: MockApiClient) {\n//            self.guestSession = .init(account: .mock(api: api))\n//        }\n//    \n//        static func mock(api: MockApiClient) -> AppState { .init(api: api) }\n//    #endif\n    \n    /// If `keepPlace` is `nil`, use the value from `UserDefaults`.\n    func changeAccount(to account: any Account, keepPlace: Bool? = nil, showAvatarPopup: Bool = true) {\n        @Setting(\\.accounts_keepPlace) var keepPlaceSetting\n        let keepPlace = keepPlace ?? keepPlaceSetting\n        \n        if firstAccount is UserAccount {\n            Task {\n                do {\n                    try await firstAccount.api.flushPostReadQueue()\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n        \n        if keepPlace {\n            if showAvatarPopup {\n                ToastModel.main.add(.account(account))\n            }\n            setAccount(to: account)\n        } else {\n            transition(account)\n            // The delays between these events are necessary to stop SwiftUIIntrospect from causing a lag spike.\n            // That library seems to not like us adding subviews to the window directly. For some reason adding\n            // these delays fixes that.\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                self.appRefreshToggle = false\n            }\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {\n                self.setAccount(to: account)\n            }\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {\n                self.appRefreshToggle = true\n            }\n        }\n    }\n    \n    private func setAccount(to account: any Account) {\n        // Save because we updated `lastUsed` in the above `deactivate()` calls\n        AccountsTracker.main.saveAccounts(ofType: .all)\n        \n        if let account = account as? UserAccount {\n            let activeAccount = UserSession(account: account)\n            if activeSessions.isEmpty {\n                guestSession.deactivate()\n            }\n            activeSessions = [activeAccount]\n        } else if let account = account as? GuestAccount {\n            activeSessions = []\n            guestSession = .init(account: account)\n            GuestAccountCache.main.clean()\n        } else {\n            assertionFailure()\n        }\n    }\n    \n    func deactivate(account: any Account) {\n        if let account = account as? UserAccount {\n            if let index = AppState.main.activeSessions.firstIndex(where: { $0.account === account }) {\n                activeSessions[index].deactivate()\n                activeSessions.remove(at: index)\n            } else { return }\n        } else if let account = account as? GuestAccount {\n            guard account == guestSession.account else { return }\n            guestSession = .init(account: AccountsTracker.main.defaultGuestAccount)\n        }\n        changeAccount(to: AccountsTracker.main.mostRecentAccount())\n    }\n    \n    var firstSession: any Session { activeSessions.first ?? guestSession }\n    var firstAccount: any Account { firstSession.account }\n    var firstApi: ApiClient { firstSession.api }\n    var firstPerson: Person? { (firstSession as? UserSession)?.person }\n    \n    var isModOrAdmin: Bool {\n        firstApi.isAdmin || !(firstPerson?.moderatedCommunities.value?.isEmpty ?? true)\n    }\n    \n    func accountThatModerates(actorId: ActorIdentifier) -> UserSession? {\n        activeSessions.first(where: { session in\n            session.person?.moderatedCommunities.value_?.contains { $0.actorId == actorId } ?? false\n        })\n    }\n    \n    func cleanCaches() {\n        for session in activeSessions {\n            session.api.cleanCaches()\n        }\n    }\n    \n    func switchToMostRecentAccount() -> Bool {\n        let mostRecentAccount = AccountsTracker.main.allAccounts\n            .filter { $0.actorId != firstAccount.actorId }\n            .min { ($0.activityState.lastUsed ?? .distantPast) > ($1.activityState.lastUsed ?? .distantPast) }\n            \n        guard let mostRecentAccount else { return false }\n        \n        changeAccount(to: mostRecentAccount)\n        return true\n    }\n    \n    var initialFeedSortType: PostSortType {\n        get async throws {\n            // In future, we should be storing `PostSortType` in `Settings` rather than `LemmySortType`\n            let defaultSort: PostSortType = .init(Settings.get(\\.post_defaultSort))\n            if try await firstApi.supports(.postSortType(defaultSort)) { return defaultSort }\n            return .init(Settings.get(\\.post_fallbackSort))\n        }\n    }\n    \n    static var main: AppState = .init()\n}\n"
  },
  {
    "path": "Mlem/App/Globals/Definitions/ErrorsTracker.swift",
    "content": "//\n//  ErrorsTracker.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-12-29.\n//\n\nimport Observation\n\n@Observable\nclass ErrorsTracker {\n    private(set) var errors: [ErrorDetails] = .init()\n    \n    @MainActor\n    func addError(_ error: Error, location: String) {\n        errors.prepend(.init(error: error, location: location))\n    }\n    \n    static var main: ErrorsTracker = .init()\n    \n    func createErrorLog() -> String {\n        var ret = \"\"\n        \n        for details in errors {\n            ret += \"\\(details.when.formatted(.iso8601))\\t\\(details.title ?? \"Error\")\\t\\(details.errorText())\\n\"\n        }\n        \n        return ret\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Globals/Definitions/FiltersTracker.swift",
    "content": "//\n//  FiltersTracker.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-12-22.\n//\n\nimport Dependencies\nimport Foundation\nimport MlemMiddleware\nimport Observation\n\n@Observable\nclass FiltersTracker {\n    @ObservationIgnored @Setting(\\.filters_keywordFilterEnabled) var keywordFilterEnabled\n    @ObservationIgnored @Setting(\\.filters_keywords) var rawKeywords {\n        didSet {\n            (self.keywords, self.phrases) = parseKeywordsAndPhrases(from: rawKeywords)\n        }\n    }\n    @ObservationIgnored @Setting(\\.filters_literalFilterEnabled) var literalFilterEnabled\n    @ObservationIgnored @Setting(\\.filters_literals) var literals\n    \n    var isAdmin: Bool\n    var moderatedCommunityActorIds: Set<ActorIdentifier>\n    \n    /// Single word keywords to filter\n    private(set) var keywords: Set<String>\n    \n    /// Multi-word phrases to filter\n    private(set) var phrases: Set<[String]>\n    \n    var filterContext: FilterContext {\n        .init(\n            isAdmin: isAdmin,\n            moderatedCommunityActorIds: moderatedCommunityActorIds,\n            filteredKeywords: keywordFilterEnabled ? keywords : .init(),\n            filteredPhrases: keywordFilterEnabled ? phrases : .init(),\n            filteredLiterals: literalFilterEnabled ? literals : .init()\n        )\n    }\n    \n    var changeHash: Int {\n        var hasher = Hasher()\n        hasher.combine(moderatedCommunityActorIds)\n        hasher.combine(rawKeywords)\n        hasher.combine(keywordFilterEnabled)\n        hasher.combine(literals)\n        hasher.combine(literalFilterEnabled)\n        return hasher.finalize()\n    }\n    \n    init() {\n        @Setting(\\.filters_keywordFilterEnabled) var keywordFilterEnabled\n        @Setting(\\.filters_keywords) var rawKeywords\n        \n        self.isAdmin = AppState.main.firstPerson?.isAdmin.value_ ?? false\n        self.moderatedCommunityActorIds = AppState.main.firstPerson?.moderatedCommunityActorIds ?? .init()\n        (self.keywords, self.phrases) = parseKeywordsAndPhrases(from: rawKeywords)\n    }\n    \n    func addFilteredKeyword(_ keyword: String) async {\n        rawKeywords.insert(keyword)\n    }\n    \n    func removeFilteredKeyword(_ keyword: String) async {\n        assert(rawKeywords.contains(keyword), \"Filtered keywords does not contain \\(keyword)\")\n        rawKeywords = rawKeywords.subtracting([keyword])\n    }\n    \n    func addFilteredLiteral(_ literal: String) async {\n        literals.insert(literal)\n    }\n    \n    func removeFilteredLiteral(_ literal: String) async {\n        assert(literals.contains(literal), \"Filtered literals do not contain \\(literal)\")\n        literals.remove(literal)\n    }\n    \n    func resetFilteredKeywords(to filteredKeywords: Set<String>) async {\n        rawKeywords = filteredKeywords\n    }\n    \n    func resetFilteredLiterals(to filteredLiterals: Set<String>) {\n        literals = filteredLiterals\n    }\n    \n    func postWouldBeFiltered(_ post: Post) -> Bool {\n        (keywordFilterEnabled && post.title.failsKeywordFilter(keywords: keywords, phrases: phrases)) ||\n        (literalFilterEnabled && post.title.failsLiteralFilter(literals: literals))\n    }\n    \n    static var main: FiltersTracker = .init()\n}\n\nprivate func parseKeywordsAndPhrases(from rawKeywords: Set<String>) -> (keywords: Set<String>, phrases: Set<[String]>) {\n    var keywords: Set<String> = .init()\n    var phrases: Set<[String]> = .init()\n    for keyword in rawKeywords {\n        if keyword.contains(\" \") {\n            phrases.insert(keyword.split(separator: \" \").map { $0.lowercased() })\n        } else {\n            keywords.insert(keyword)\n        }\n    }\n    return (keywords, phrases)\n}\n"
  },
  {
    "path": "Mlem/App/Globals/Definitions/PaletteOption.swift",
    "content": "//\n//  PaletteOption.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-08.\n//\n\nimport Foundation\nimport SwiftUI\nimport Theming\n\nenum PaletteOption: String, CaseIterable, Codable {\n    case standard, oled, monochrome, solarized, dracula\n    \n    var palette: Palette {\n        switch self {\n        case .standard: .default\n        case .oled: .oled\n        case .monochrome: .monochrome\n        case .solarized: .solarized\n        case .dracula: .dracula\n        }\n    }\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .standard: \"Default\"\n        case .oled: \"OLED\"\n        case .monochrome: \"Monochrome\"\n        case .solarized: \"Solarized\"\n        case .dracula: \"Dracula\"\n        }\n    }\n    \n    var supportedModes: UIUserInterfaceStyle {\n        switch self {\n        case .oled, .dracula: .dark\n        default: .unspecified\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Globals/Definitions/PersistenceRepository.swift",
    "content": "//\n//  PersistenceRepository.swift\n//  Mlem\n//\n//  Created by mormaer on 26/07/2023.\n//\n//\n\nimport Dependencies\nimport Foundation\nimport MlemMiddleware\n\nenum PersistencePath {\n    static var root = {\n        guard let path = try? FileManager.default.url(\n            for: .applicationSupportDirectory,\n            in: .userDomainMask,\n            appropriateFor: nil,\n            create: true\n        ) else {\n            fatalError(\"unable to access application support path\")\n        }\n\n        return path\n    }()\n\n    static var userAccounts = root.appendingPathComponent(\"Saved Accounts\", conformingTo: .json)\n    static var guestAccounts = root.appendingPathComponent(\"Guest Accounts\", conformingTo: .json)\n    static var favoriteCommunities = root.appendingPathComponent(\"Favorite Communities\", conformingTo: .json)\n    static var instanceMetadata = root.appendingPathComponent(\"Instance Metadata\", conformingTo: .json)\n    static var layoutWidgets = root.appendingPathComponent(\"Layout Widgets\", conformingTo: .json)\n    static var pinnedSortTypes = root.appendingPathComponent(\"Sort Settings\", conformingTo: .json)\n    static var systemSettings = root.appendingPathComponent(\"System Settings\", conformingTo: .directory)\n    \n    static func accountSettingsDirectory(for account: any Account) -> URL {\n        root\n            .appendingPathComponent(\"Account Settings\", conformingTo: .directory)\n            .appendingPathComponent(account.uniqueStringId, conformingTo: .directory)\n    }\n    \n    static func accountSettings(for account: any Account) -> URL {\n        accountSettingsDirectory(for: account)\n            .appendingPathComponent(\"Settings\", conformingTo: .json)\n    }\n    \n    static func visitHistory(for account: any Account) -> URL {\n        accountSettingsDirectory(for: account)\n            .appendingPathComponent(\"Visit History\", conformingTo: .json)\n    }\n}\n\nprivate enum DiskAccess {\n    static func load(from path: URL) throws -> Data {\n        try Data(contentsOf: path, options: .mappedIfSafe)\n    }\n    \n    static func save(_ data: Data, to path: URL) async throws {\n        try await Task(priority: .background) {\n            try FileManager.default.createDirectory(\n                at: path.deletingLastPathComponent(),\n                withIntermediateDirectories: true\n            )\n            try data.write(to: path, options: .atomic)\n        }\n        .value\n    }\n}\n\n// Enumeration of system-managed settings\nenum SystemSetting {\n    /// v1 settings manually saved by the user\n    case v1_user\n    /// v2 settings manually saved by the user\n    case v2_user\n    /// v2 settings automatically saved by the app\n    case v2_system\n    \n    var path: String {\n        switch self {\n        case .v1_user: \"v1\"\n        case .v2_user: \"v2\"\n        case .v2_system: \"v2_system\"\n        }\n    }\n}\n\nclass PersistenceRepository {\n    enum PersistenceRepositoryError: Error {\n        case noFullName\n    }\n    \n    @Dependency(\\.date) private var date\n    \n    private var keychainAccess: (String) -> String?\n    private var read: (URL) throws -> Data\n    private var write: (Data, URL) async throws -> Void\n    private let bundle: Bundle\n    \n    init(\n        keychainAccess: @escaping (String) -> String?,\n        read: @escaping (URL) throws -> Data = { try DiskAccess.load(from: $0) },\n        write: @escaping (Data, URL) async throws -> Void = { try await DiskAccess.save($0, to: $1) },\n        bundle: Bundle = Bundle.main\n    ) {\n        self.keychainAccess = keychainAccess\n        self.read = read\n        self.write = write\n        self.bundle = bundle\n        \n        // set up settings directories--if this fails, something has gone _terribly_ wrong\n        do {\n            try FileManager.default.createDirectory(at: PersistencePath.systemSettings, withIntermediateDirectories: true)\n        } catch {\n            fatalError(\"Could not create settings directories\")\n        }\n    }\n    \n    // MARK: - Public methods\n    \n    func deleteAccountSettings(for account: any Account) throws {\n        try FileManager.default.removeItem(at: PersistencePath.accountSettingsDirectory(for: account))\n    }\n    \n    func deleteVisitHistory(for account: any Account) throws {\n        try FileManager.default.removeItem(at: PersistencePath.visitHistory(for: account))\n    }\n    \n    func loadUserAccounts() -> [UserAccount] {\n        load([UserAccount].self, from: PersistencePath.userAccounts) ?? []\n    }\n    \n    func saveUserAccounts(_ value: [UserAccount]) async throws {\n        try await save(value, to: PersistencePath.userAccounts)\n    }\n    \n    func loadGuestAccounts() -> [GuestAccount] {\n        load([GuestAccount].self, from: PersistencePath.guestAccounts) ?? []\n    }\n    \n    func saveGuestAccounts(_ value: [GuestAccount]) async throws {\n        try await save(value, to: PersistencePath.guestAccounts)\n    }\n\n    func loadInteractionBarConfigurations() -> InteractionBarConfigurations {\n        if let standard = load(InteractionBarConfigurations.self, from: PersistencePath.layoutWidgets, silentError: true) {\n            return standard\n        }\n        return .default\n    }\n    \n    func saveInteractionBarConfigurations(_ value: InteractionBarConfigurations) async throws {\n        try await save(value, to: PersistencePath.layoutWidgets)\n    }\n    \n    func loadVisitHistory(for account: UserAccount) async throws -> VisitHistory {\n        let path = PersistencePath.visitHistory(for: account)\n        let data = load(VisitHistory.CodedData.self, from: path, silentError: true) ?? .init()\n        return try await .init(data: data, api: account.api)\n    }\n    \n    func saveVisitHistory(_ visitHistory: VisitHistory, for account: UserAccount) async throws {\n        let path = PersistencePath.visitHistory(for: account)\n        try await save(visitHistory.codedData(), to: path)\n    }\n    \n    func loadPinnedSortTypes() -> Set<PostSortType> {\n        let apiSortTypes = load(Set<LemmySortType>.self, from: PersistencePath.pinnedSortTypes) ?? [\n            .hot, .new, .topSixHour, .topDay, .topWeek, .topMonth, .topYear, .topAll\n        ]\n        return Set(apiSortTypes.map(PostSortType.init))\n    }\n    \n    func savePinnedSortTypes(_ value: Set<PostSortType>) async throws {\n        try await save(value.compactMap(\\.v3ApiType), to: PersistencePath.pinnedSortTypes)\n    }\n    \n    /// Saves the given user settings\n    func saveAccountSettings(_ settings: SettingsValues, for account: any Account) async throws {\n        try await save(settings, to: PersistencePath.accountSettings(for: account))\n    }\n    \n    /// Loads given user settings, if present\n    func loadAccountSttings(for account: any Account) -> SettingsValues? {\n        load(SettingsValues.self, from: PersistencePath.accountSettings(for: account))\n    }\n    \n    /// Returns true if the given system settings exist, false otherwise\n    func systemSettingsExists(_ setting: SystemSetting) -> Bool {\n        // FileManager does offer fileExists but it always returns false, this way is reliable\n        if loadSystemSettings(setting) != nil { return true }\n        return false\n    }\n    \n    /// Saves the given system settings\n    func saveSystemSettings(_ settings: SettingsValues, setting: SystemSetting) async throws {\n        try await save(settings, to: PersistencePath.systemSettings.appendingPathComponent(setting.path, conformingTo: .json))\n    }\n    \n    /// Loads given system settings, if present\n    func loadSystemSettings(_ setting: SystemSetting) -> SettingsValues? {\n        load(SettingsValues.self, from: PersistencePath.systemSettings.appendingPathComponent(setting.path, conformingTo: .json))\n    }\n    \n    // DEV ONLY\n    func deleteAllSystemSettings() throws {\n        try FileManager.default.removeItem(at: PersistencePath.systemSettings)\n        try FileManager.default.createDirectory(at: PersistencePath.systemSettings, withIntermediateDirectories: true)\n    }\n\n//\n//    func loadInstanceMetadata() -> TimestampedValue<[InstanceMetadata]> {\n//        let localFile = load(TimestampedValue<[InstanceMetadata]>.self, from: Path.instanceMetadata)\n//        let bundledFile = loadFromBundle(TimestampedValue<[InstanceMetadata]>.self, filename: \"instance_metadata\")\n//\n//        if let localFile, localFile.timestamp > bundledFile.timestamp {\n//            return localFile\n//        }\n//\n//        return bundledFile\n//    }\n//\n//    func saveInstanceMetadata(_ value: [InstanceMetadata]) async throws {\n//        let timestamped = TimestampedValue(value: value, timestamp: date.now, lifespan: .days(1))\n//        try await save(timestamped, to: Path.instanceMetadata)\n//    }\n    \n    // MARK: Loading methods\n    \n    func load<T: Decodable>(_ model: T.Type, from path: URL, silentError: Bool = false) -> T? {\n        do {\n            let data = try read(path)\n            \n            guard !data.isEmpty else {\n                return nil\n            }\n            \n            return try JSONDecoder().decode(T.self, from: data)\n        } catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == 260 {\n            // Don't show error toast if file not found\n            return nil\n        } catch {\n            handleError(error, silent: silentError)\n            return nil\n        }\n    }\n    \n    private func loadFromBundle<T: Decodable>(_ model: T.Type, filename: String, type: String = \"json\") -> T {\n        do {\n            let path = bundle.path(forResource: filename, ofType: type)!\n            let stringValue = try String(contentsOfFile: path)\n            let data = stringValue.data(using: .utf8)!\n            return try JSONDecoder().decode(T.self, from: data)\n        } catch {\n            fatalError(\"☠️ failed to load \\(filename).\\(type) from the application bundle.\")\n        }\n    }\n    \n    func save(_ value: some Encodable, to path: URL) async throws {\n        do {\n            let encoder = JSONEncoder()\n            encoder.userInfo[.endpointVersion] = LemmyEndpointVersion.v3\n            let data = try encoder.encode(value)\n            try await write(data, path)\n        } catch {\n            handleError(error)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Globals/Definitions/TabReselectTracker.swift",
    "content": "//\n//  TabReselectTracker.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-11-02.\n//\n\nimport Foundation\nimport SwiftUI\n\n@Observable\nclass TabReselectTracker {\n    var blockTabSwitch: Bool = false\n    private(set) var flag: Bool = false\n    var consumers: Int = 0\n\n    static var main: TabReselectTracker = .init()\n\n    func signal() {\n        flag = true\n    }\n    \n    func reset() {\n        flag = false\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Globals/Dependencies/PersistenceRepository+Dependency.swift",
    "content": "//\n//  PersistenceRepository+Dependency.swift\n//  Mlem\n//\n//  Created by mormaer on 26/07/2023.\n//\n//\n\nimport Dependencies\nimport Foundation\n\nextension PersistenceRepository: DependencyKey {\n    static let liveValue = PersistenceRepository(keychainAccess: { Constants.main.keychain[$0] })\n}\n\nextension DependencyValues {\n    var persistenceRepository: PersistenceRepository {\n        get { self[PersistenceRepository.self] }\n        set { self[PersistenceRepository.self] = newValue }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Legacy/LegacySettings.swift",
    "content": "//\n//  Settings.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-07.\n//  Adapted from https://fatbobman.com/en/posts/appstorage/\n//\n\nimport Dependencies\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nclass LegacySettings: ObservableObject {\n    @Dependency(\\.persistenceRepository) var persistenceRepository\n    \n    static let main: LegacySettings = .init()\n    \n    /// Default initializer. Will take current AppStorage values.\n    init() {}\n\n    @AppStorage(\"a11y.readPostIndicator\") var readPostIndicator: ReadPostIndicator = .checkmark\n    @AppStorage(\"a11y.readOutlineThickness\") var readOutlineThickness: Int = 3\n    @AppStorage(\"a11y.showSettingsIcons\") var showSettingsIcons: Bool = false\n    @AppStorage(\"a11y.websiteThumbnailIcon\") var websiteThumbnailIcon: Bool = false\n    @AppStorage(\"a11y.zoomSliderLocation\") var zoomSliderLocation: ZoomSliderLocation = .none\n\n    @AppStorage(\"post.size\") var postSize: PostSize = .large\n    @AppStorage(\"post.allowMultipleColumns\") var allowMultiplePostColumns: Bool = true\n    @AppStorage(\"post.defaultSort\") var defaultPostSort: LemmySortType = .hot\n    @AppStorage(\"post.fallbackSort\") var fallbackPostSort: LemmySortType = .hot\n    @AppStorage(\"post.thumbnailLocation\") var thumbnailLocation: ThumbnailLocation = .left\n    @AppStorage(\"post.showCreator\") var showPostCreator: Bool = false\n    @AppStorage(\"post.showSubscribedStatus\") var showSubscribedStatus: Bool = true\n    @AppStorage(\"post.showDownvotesCompact\") var showDownvotesCompact: Bool = false\n    @AppStorage(\"post.gestures.tapToCollapse\") var tapPostsToCollapse: Bool = true\n\n    @AppStorage(\"quickSwipes.enabled\") var quickSwipesEnabled: Bool = true\n    \n    @AppStorage(\"behavior.hapticLevel\") var hapticLevel: HapticTier = .high\n    @AppStorage(\"behavior.upvoteOnSave\") var upvoteOnSave: Bool = false\n    @AppStorage(\"behavior.internetSpeed\") var internetSpeed: InternetSpeed = .fast\n    @AppStorage(\"behavior.autoplayMedia\") var autoplayMedia: Bool = false\n    @AppStorage(\"behavior.muteVideos\") var muteVideos: Bool = true\n    @AppStorage(\"behavior.confirmImageUploads\") var confirmImageUploads: Bool = true\n    @AppStorage(\"behavior.infiniteScroll\") var infiniteScroll: Bool = true\n    \n    @AppStorage(\"accounts.keepPlace\") var keepPlaceOnAccountSwitch: Bool = false\n    @AppStorage(\"accounts.sort\") var accountSort: AccountSortMode = .name\n    @AppStorage(\"accounts.groupSort\") var groupAccountSort: Bool = false\n    \n    @AppStorage(\"appearance.interfaceStyle\") var interfaceStyle: UIUserInterfaceStyle = .unspecified\n    @AppStorage(\"appearance.palette\") var colorPalette: PaletteOption = .standard\n    \n    @AppStorage(\"markdown.wrapCodeBlockLines\") var wrapCodeBlockLines: Bool = true\n    \n    @AppStorage(\"dev.developerMode\") var developerMode: Bool = false\n    \n    @AppStorage(\"safety.blurNsfw\") var blurNsfw: NsfwBlurBehavior = .always\n    @AppStorage(\"safety.showNsfwCommunityWarning\") var showNsfwCommunityWarning: Bool = true\n    @AppStorage(\"safety.showModlogWarning\") var showModlogWarning: Bool = true\n    \n    @AppStorage(\"privacy.autoBypassImageProxy\") var autoBypassImageProxy: Bool = false\n    @AppStorage(\"privacy.showFavicons\") var showFavicons: Bool = true\n    \n    @AppStorage(\"links.openInBrowser\") var openLinksInBrowser = false\n    @AppStorage(\"links.readerMode\") var openLinksInReaderMode = false\n    @AppStorage(\"links.displayMode\") var tappableLinksDisplayMode: TappableLinksDisplayMode = .contextual\n    @AppStorage(\"links.shareMode\") var linkSharingMode: LinkSharingMode = .myInstance\n    @AppStorage(\"links.embedLoops\") var embedLoops: Bool = true\n    \n    // swiftlint:disable:next line_length\n    @AppStorage(\"media.animatedAvatars\") var animatedAvatars: AnimatedAvatarBehavior = UIAccessibility.isReduceMotionEnabled ? .never : .always\n    \n    @AppStorage(\"feed.markReadOnScroll\") var markReadOnScroll: Bool = false\n    @AppStorage(\"feed.showRead\") var showReadInFeed: Bool = true\n    @AppStorage(\"feed.default\") var defaultFeed: ListingType = .subscribed\n    \n    @AppStorage(\"inbox.showRead\") var showReadInInbox: Bool = true\n    \n    @AppStorage(\"subscriptions.instanceLocation\") var subscriptionInstanceLocation: InstanceLocation = UIDevice.isPad ? .bottom : .trailing\n    \n    @AppStorage(\"subscriptions.sort\") var subscriptionSort: SubscriptionListSort = .alphabetical\n    \n    @AppStorage(\"person.showAvatar\") var showPersonAvatar: Bool = true\n    \n    @AppStorage(\"community.showAvatar\") var showCommunityAvatar: Bool = true\n    \n    @AppStorage(\"comment.compact\") var compactComments: Bool = false\n    @AppStorage(\"comment.jumpButton\") var jumpButton: CommentJumpButtonLocation = .bottomTrailing\n    @AppStorage(\"comment.sort\") var commentSort: LemmyCommentSortType = .top\n    @AppStorage(\"comment.maxDepth\") var maxCommentDepth: Int = 8\n    @AppStorage(\"comment.gestures.tapToCollapse\") var tapCommentsToCollapse: Bool = true\n    \n    @AppStorage(\"status.bypassImageProxyShown\") var bypassImageProxyShown: Bool = false\n    \n    @AppStorage(\"tip.feedWelcomePrompt\") var showFeedWelcomePrompt: Bool = true\n    \n    @AppStorage(\"navigation.sidebarVisibleByDefault\") var sidebarVisibleByDefault: Bool = true\n    @AppStorage(\"navigation.swipeAnywhere\") var swipeAnywhereToNavigate: Bool = false\n    \n    @AppStorage(\"tab.profile.labelType\") var tabProfileLabelType: ProfileTabLabel = .nickname\n    @AppStorage(\"tab.profile.showAvatar\") var tabProfileShowAvatar: Bool = true\n    @AppStorage(\"tab.inbox.badgeIncludedTypes\") var tabInboxBadgeIncludedTypes: Set<InboxItemType> = .all\n    \n    @AppStorage(\"menus.moderatorActionGrouping\") var moderatorActionGrouping: ModeratorActionGrouping = .divider\n    @AppStorage(\"menus.allModActions\") var showAllModActions: Bool = false\n    \n    @AppStorage(\"interactionBar.alternateReportLayout\") var alternateInteractionBarLayoutForReports: Bool = false\n    \n    @AppStorage(\"filters.keywordFilterEnabled\") var keywordFilterEnabled: Bool = true\n}\n"
  },
  {
    "path": "Mlem/App/Logic/Animations.swift",
    "content": "//\n//  Animations.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-12-29.\n//\n\nimport SwiftUI\nimport UIKit\n\n// https://stackoverflow.com/a/72973172\n/// Disables animations on the given action\nfunc withoutAnimation(action: @escaping () -> Void) {\n    var transaction = Transaction()\n    transaction.disablesAnimations = true\n    withTransaction(transaction) {\n        action()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Logic/HandleError.swift",
    "content": "//\n//  HandleError.swift\n//  Mlem\n//\n//  Created by Sjmarf on 18/05/2024.\n//\n\nimport MlemMiddleware\nimport os\nimport SwiftUI\n\nfunc handleError(\n    _ error: Error,\n    silent: Bool = false,\n    file: String = #fileID,\n    function: String = #function,\n    line: Int = #line\n) {\n    if !_handleError(error, file: file, function: function, line: line), !silent {\n        ToastModel.main.add(.error(.init(error: error)))\n    }\n}\n\nfunc handleErrorWithDetails(\n    _ error: Error,\n    file: String = #fileID,\n    function: String = #function,\n    line: Int = #line\n) -> ErrorDetails? {\n    if !_handleError(error, file: file, function: function, line: line) {\n        return .init(error: error)\n    }\n    return nil\n}\n\n/// - Returns: true if no further handling is required, false otherwise\nprivate func _handleError(\n    _ error: Error,\n    file: String = #fileID,\n    function: String = #function,\n    line: Int = #line\n) -> Bool {\n    #if DEBUG\n        let descriptiveString: String\n        if let error = error as? ApiClientError {\n            descriptiveString = \"     \\(String(describing: error))\\n\"\n        } else {\n            descriptiveString = \"\"\n        }\n        let statement = \"\"\"\n        ☠️ ERROR ☠️\n        📝 -> \\(error.localizedDescription)\n        \\(descriptiveString)📂 -> \\(file) | \\(function) | line: \\(line)\n        \"\"\"\n        Logger.universal.error(\"\\(statement)\")\n    #endif\n    \n    let location = \"\\(file), \\(function):\\(line)\"\n    \n    Task {\n        await ErrorsTracker.main.addError(error, location: location)\n    }\n    \n    switch error {\n    // TODO: Modify MlemMiddleware to attach the ApiClient throwing the error to ApiClientError.invalidSession, so that we can access the relevant UserStub in a multi-account context\n    case ApiClientError.invalidSession, ApiClientError.noToken, UserAccount.DecodingError.noTokenInKeychain:\n        Task { @MainActor in\n            showReauthSheet()\n        }\n        return true\n    case ApiClientError.cancelled, is CancellationError:\n        print(\"Cancellation error\")\n        return true\n    default:\n        if (error as NSError).code == NSURLErrorCancelled {\n            print(\"Timeout error\")\n            return true\n        }\n        return false\n    }\n}\n\n@MainActor\nprivate func showReauthSheet() {\n    if let user = AppState.main.firstSession.account as? UserAccount,\n       !NavigationModel.main.layers.contains(where: { $0.root == .logIn(.reauth(user)) }) {\n        NavigationModel.main.openSheet(.logIn(.reauth(user)))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Logic/ImageFunctions.swift",
    "content": "//\n//  ImageFunctions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-25.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport Nuke\nimport Photos\nimport Rest\nimport SwiftUI\n\nfunc saveMedia(url: URL) async {\n    do {\n        let (data, _) = try await ImagePipeline.shared.data(for: .init(urlRequest: mlemUrlRequest(url: url)))\n        let imageSaver = ImageSaver()\n        if url.pathExtension.isMovieExtension {\n            try await imageSaver.writeVideoToPhotoAlbum(url: url)\n            ToastModel.main.add(.success(\"Video Saved\"))\n        } else {\n            try await imageSaver.writeImageToPhotoAlbum(imageData: data)\n            ToastModel.main.add(.success(\"Image Saved\"))\n        }\n    } catch {\n        handleError(error, silent: true)\n        ToastModel.main.add(.basic(\n            \"Failed to save media\",\n            subtitle: \"You may need to allow Mlem to access your Photo Library in System Settings.\",\n            color: .themedNegative,\n            duration: 5\n        ))\n    }\n}\n\n@MainActor\nfunc createImageFromView(_ view: some View, dimensions: CGSize? = nil) -> UIImage? {\n    let renderer = ImageRenderer(content: view)\n    renderer.scale = 3 // boost resolution to look better on larger devices\n    if let dimensions {\n        renderer.proposedSize = .init(dimensions)\n    } else {\n        // assume screen width\n        renderer.proposedSize.width = UIScreen.main.bounds.width\n    }\n    return renderer.uiImage\n}\n\nfunc shareImage(url: URL, navigation: NavigationLayer) async {\n    if let fileUrl = await downloadImageToFileSystem(url: url) {\n        navigation.model?.shareInfo = .init(url: fileUrl)\n    }\n}\n\nfunc fullSizeUrl(url: URL?) -> URL? {\n    if let url, var components = URLComponents(url: url, resolvingAgainstBaseURL: true) {\n        components.queryItems = components.queryItems?.filter { $0.name != \"thumbnail\" }\n        return components.url\n    }\n    return nil\n}\n\n/// Downloads the image at the given URL to the file system, returning the path to the downloaded image\nfunc downloadImageToFileSystem(url: URL) async -> URL? {\n    do {\n        let (data, _) = try await ImagePipeline.shared.data(for: .init(urlRequest: mlemUrlRequest(url: url)))\n        var fileName: String\n        \n        // image proxies that use url query param don't have pathExtension so we extract it from the embedded url\n        if url.pathExtension.isEmpty,\n           let components = URLComponents(url: url, resolvingAgainstBaseURL: true),\n           let queryItems = components.queryItems,\n           let baseUrlString = queryItems.first(where: { $0.name == \"url\" })?.value,\n           let baseUrl = URL(string: baseUrlString) {\n            fileName = baseUrl.lastPathComponent\n        } else {\n            fileName = url.lastPathComponent\n        }\n        \n        if fileName.isEmpty {\n            assertionFailure(\"Empty fileName!\")\n            return nil\n        }\n        \n        return try data.writeToTempFile(fileName: fileName)\n    } catch {\n        handleError(error)\n        return nil\n    }\n}\n\nfunc downloadTextToFileSystem(fileName: String, text: String) async -> URL? {\n    do {\n        let fileUrl = FileManager.default.temporaryDirectory.appending(path: fileName)\n        if FileManager.default.fileExists(atPath: fileUrl.absoluteString) {\n            try FileManager.default.removeItem(at: fileUrl)\n        }\n        try text.write(to: fileUrl, atomically: true, encoding: String.Encoding.utf8)\n        return fileUrl\n    } catch {\n        handleError(error)\n        return nil\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Logic/ImageSaver.swift",
    "content": "//\n//  ImageSaver.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-11-15.\n//  Adapted from https://www.hackingwithswift.com/books/ios-swiftui/how-to-save-images-to-the-users-photo-library\n//\n\nimport Foundation\nimport Photos\n\nclass ImageSaver: NSObject {\n    func writeVideoToPhotoAlbum(url: URL) async throws {\n        guard let tempFile = await downloadImageToFileSystem(url: url) else {\n            ToastModel.main.add(.error(.init(title: \"Failed to save video\")))\n            return\n        }\n        \n        try await PHPhotoLibrary.shared().performChanges {\n            _ = PHAssetCreationRequest.creationRequestForAssetFromVideo(atFileURL: tempFile)\n        }\n    }\n    \n    func writeImageToPhotoAlbum(imageData: Data) async throws {\n        try await PHPhotoLibrary.shared().performChanges {\n            let creationRequest = PHAssetCreationRequest.forAsset()\n            creationRequest.addResource(with: .photo, data: imageData, options: nil)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Logic/Networking/InternetConnectionManager.swift",
    "content": "//\n//  Reachibility.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2023.\n//\n\nimport Foundation\nimport SystemConfiguration\n\npublic enum InternetConnectionManager {\n    public static func isConnectedToNetwork() -> Bool {\n        var zeroAddress = sockaddr_in()\n        zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))\n        zeroAddress.sin_family = sa_family_t(AF_INET)\n        guard let defaultRouteReachability = withUnsafePointer(to: &zeroAddress, {\n            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {\n                SCNetworkReachabilityCreateWithAddress(nil, $0)\n            }\n            \n        }) else {\n            return false\n        }\n        var flags = SCNetworkReachabilityFlags()\n        if !SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags) {\n            return false\n        }\n        let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0\n        let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0\n        return isReachable && !needsConnection\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Account/Account.swift",
    "content": "//\n//  NewSavedUser.swift\n//  Mlem\n//\n//  Created by Sjmarf on 10/02/2024.\n//\n\nimport Foundation\nimport KeychainAccess\nimport MlemMiddleware\nimport SwiftUI\n\nprotocol Account: AnyObject, Codable, ActorIdentifiable, ProfileProviding, Hashable {\n    // Stored\n    var api: ApiClient { get }\n    var name: String { get }\n    var storedNickname: String? { get }\n    var siteSoftware: SiteSoftware? { get }\n    var avatar: URL? { get }\n    var activityState: AccountActivityState { get set }\n    var accountType: AccountType { get }\n    \n    // Computed\n    var nickname: String { get }\n    var nicknameSortKey: String { get }\n    var instanceSortKey: String { get }\n    var isActive: Bool { get }\n    var uniqueStringId: String { get }\n    \n    func setNickname(_ newValue: String)\n}\n\nenum AccountActivityState: Codable, Hashable {\n    case inactive(lastUsed: Date?)\n    case active\n    \n    var lastUsed: Date? {\n        switch self {\n        case let .inactive(lastUsed: lastUsed): lastUsed\n        case .active: nil\n        }\n    }\n}\n\n// ProfileProviding conformance\nextension Account {\n    var blocked: any RealizedValueProviding<Bool> { RealizedValue(false) }\n    var displayName: String { name }\n}\n\nextension Account {\n    func hash(into hasher: inout Hasher) {\n        hasher.combine(actorId)\n    }\n    \n    static func == (lhs: Self, rhs: Self) -> Bool {\n        lhs.actorId == rhs.actorId\n    }\n}\n\nextension Account {\n    func signOut() {\n        AccountsTracker.main.removeAccount(account: self)\n    }\n    \n    func activate() {\n        activityState = .active\n    }\n    \n    func deactivate() {\n        activityState = .inactive(lastUsed: .now)\n    }\n    \n    var nickname: String { storedNickname ?? name }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Account/AccountType.swift",
    "content": "//\n//  AccountType.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-10-17.\n//\n\nenum AccountType: String, Codable, Comparable {\n    case guest, user, moderator, admin\n    \n    private var tier: Int {\n        switch self {\n        case .guest: 0\n        case .user: 1\n        case .moderator: 2\n        case .admin: 3\n        }\n    }\n    \n    static func < (lhs: AccountType, rhs: AccountType) -> Bool {\n        lhs.tier < rhs.tier\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Account/GuestAccount.swift",
    "content": "//\n//  GuestAccount.swift\n//  Mlem\n//\n//  Created by Sjmarf on 24/05/2024.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport Observation\n\n@Observable\nclass GuestAccount: Account {\n    let actorId: ActorIdentifier\n    let api: ApiClient\n    var storedNickname: String?\n    var siteSoftware: SiteSoftware?\n    var avatar: URL?\n    var activityState: AccountActivityState\n    let accountType: AccountType = .guest\n    \n    fileprivate init(url: URL) throws {\n        guard let host = url.host() else { throw DecodingError.invalidHost }\n        self.actorId = .instance(host: host)\n        self.activityState = .inactive(lastUsed: nil)\n        self.api = .getApiClient(url: url, username: nil)\n    }\n  \n    // TODO: updated mocks\n//    #if DEBUG\n//        private init(api: MockApiClient) {\n//            self.actorId = api.actorId\n//            self.activityState = .inactive(lastUsed: nil)\n//            self.api = api\n//        }\n//    \n//        static func mock(api: MockApiClient) -> GuestAccount { .init(api: api) }\n//    #endif\n    \n    static func getGuestAccount(url: URL) throws -> GuestAccount {\n        try GuestAccountCache.main.getAccount(url: url)\n    }\n    \n    enum CodingKeys: String, CodingKey {\n        // Keys are named this way to be consistent with the `UserAccount.CodingKey` cases\n        case storedNickname, instanceLink, siteVersion, avatarUrl, lastUsed, activityState, siteSoftware\n    }\n    \n    enum DecodingError: Error {\n        case invalidHost\n    }\n    \n    required init(from decoder: Decoder) throws {\n        let values = try decoder.container(keyedBy: CodingKeys.self)\n        \n        self.storedNickname = try values.decode(String?.self, forKey: .storedNickname)\n        \n        if let siteSoftware = try values.decodeIfPresent(SiteSoftware.self, forKey: .siteSoftware) {\n            self.siteSoftware = siteSoftware\n        } else if let version = try values.decode(SiteVersion?.self, forKey: .siteVersion) {\n            self.siteSoftware = .init(type: .lemmy, version: version)\n        } else {\n            self.siteSoftware = nil\n        }\n        \n        self.avatar = try values.decode(URL?.self, forKey: .avatarUrl)\n        \n        if let activityState = try values.decodeIfPresent(AccountActivityState.self, forKey: .activityState) {\n            self.activityState = activityState\n        } else {\n            let lastUsed = try values.decodeIfPresent(Date?.self, forKey: .lastUsed) ?? nil\n            self.activityState = .inactive(lastUsed: lastUsed)\n        }\n\n        let actorId = try values.decode(ActorIdentifier.self, forKey: .instanceLink)\n        self.actorId = actorId\n        self.api = ApiClient.getApiClient(url: actorId.url, username: nil)\n        GuestAccountCache.main.itemCache.put(self)\n    }\n    \n    func encode(to encoder: Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(storedNickname, forKey: .storedNickname)\n        try container.encode(siteSoftware, forKey: .siteSoftware)\n        try container.encode(avatar, forKey: .avatarUrl)\n        try container.encode(activityState, forKey: .activityState)\n        try container.encode(api.baseUrl, forKey: .instanceLink)\n    }\n    \n    @MainActor\n    func update(instance: Instance, software: SiteSoftware) {\n        var shouldSave = false\n        if avatar != instance.avatar {\n            avatar = instance.avatar\n            shouldSave = true\n        }\n        if siteSoftware != software {\n            siteSoftware = software\n            shouldSave = true\n        }\n        if shouldSave {\n            AccountsTracker.main.saveAccounts(ofType: .guest)\n        }\n    }\n    \n    var name: String { actorId.host }\n    \n    var isActive: Bool { AppState.main.guestSession === self }\n    \n    var isSaved: Bool {\n        AccountsTracker.main.guestAccounts.contains(where: { $0 === self })\n    }\n    \n    var nicknameSortKey: String { storedNickname ?? name }\n    var instanceSortKey: String { host }\n    \n    var uniqueStringId: String { host }\n    \n    func resetStoredSettings(withSave: Bool = true) {\n        storedNickname = nil\n        if withSave {\n            AccountsTracker.main.saveAccounts(ofType: .guest)\n        }\n    }\n    \n    func setNickname(_ newValue: String) {\n        storedNickname = newValue.isEmpty ? nil : newValue\n        AccountsTracker.main.saveAccounts(ofType: .guest)\n    }\n    \n    var profileCreated: Date? { nil }\n    var description: String? { nil }\n    var banner: URL? { nil }\n    var updated: Date? { nil }\n}\n\nextension GuestAccount: CacheIdentifiable {\n    var cacheId: Int { actorId.hashValue }\n}\n\nclass GuestAccountCache: CoreCache<GuestAccount> {\n    static let main: GuestAccountCache = .init()\n    \n    func getAccount(url: URL) throws -> GuestAccount {\n        if let account = retrieveModel(cacheId: url.hashValue) {\n            return account\n        }\n        let account = try GuestAccount(url: url)\n        itemCache.put(account)\n        return account\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Account/UserAccount.swift",
    "content": "//\n//  AuthenticatedAccount.swift\n//  Mlem\n//\n//  Created by Sjmarf on 24/05/2024.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport Observation\n\n@Observable\nclass UserAccount: Account, CommunityOrPerson {\n    static var identifierPrefix: String = \"@\"\n    \n    let actorId: ActorIdentifier\n    let id: Int\n    let api: ApiClient\n    let name: String\n    var storedNickname: String?\n    var siteSoftware: SiteSoftware?\n    var avatar: URL?\n    var activityState: AccountActivityState\n    var favorites: Set<Int>\n    var visitHistoryEnabled: Bool\n    var accountType: AccountType\n    var description: String?\n    var banner: URL?\n    var created: Date?\n    var updated: Date?\n    \n    init(person: Person, siteSoftware: SiteSoftware) {\n        self.api = person.api\n        self.id = person.id\n        self.name = person.name\n        self.actorId = person.actorId\n        self.storedNickname = nil\n        self.siteSoftware = siteSoftware\n        self.avatar = person.avatar\n        self.activityState = .inactive(lastUsed: nil)\n        self.favorites = []\n        self.visitHistoryEnabled = true\n        self.accountType = (person.moderatedCommunities.value_?.isEmpty ?? true) ? .user : .moderator\n        self.description = person.description\n        self.banner = person.banner\n        self.created = person.created\n        self.updated = person.updated\n    }\n    \n    enum CodingKeys: String, CodingKey {\n        // These key names don't match the identifiers of their corresponding properties - this is because these key names must match the property names used in SavedAccount pre-1.3 in order to maintain compatibility\n        case id, username, storedNickname, instanceLink, siteVersion, avatarUrl\n        case lastUsed, favorites, accountType, visitHistoryEnabled, activityState\n        case siteSoftware\n        case description, banner, created, updated\n    }\n    \n    enum DecodingError: Error { case cannotModifyPathComponents, invalidHost, noTokenInKeychain }\n    \n    required init(from decoder: Decoder) throws {\n        let values = try decoder.container(keyedBy: CodingKeys.self)\n        \n        // copy simple values\n        self.id = try values.decode(Int.self, forKey: .id)\n        let name = try values.decode(String.self, forKey: .username)\n        self.name = name\n        self.storedNickname = try values.decode(String?.self, forKey: .storedNickname)\n        \n        if let siteSoftware = try values.decodeIfPresent(SiteSoftware.self, forKey: .siteSoftware) {\n            self.siteSoftware = siteSoftware\n        } else if let version = try values.decode(SiteVersion?.self, forKey: .siteVersion) {\n            self.siteSoftware = .init(type: .lemmy, version: version)\n        } else {\n            self.siteSoftware = nil\n        }\n        \n        self.avatar = try values.decode(URL?.self, forKey: .avatarUrl)\n        \n        if let activityState = try values.decodeIfPresent(AccountActivityState.self, forKey: .activityState) {\n            self.activityState = activityState\n        } else {\n            let lastUsed = try values.decodeIfPresent(Date?.self, forKey: .lastUsed) ?? nil\n            self.activityState = .inactive(lastUsed: lastUsed)\n        }\n        \n        self.favorites = try values.decodeIfPresent(Set<Int>.self, forKey: .favorites) ?? []\n        self.visitHistoryEnabled = try values.decodeIfPresent(Bool.self, forKey: .visitHistoryEnabled) ?? true\n        self.accountType = try values.decodeIfPresent(AccountType.self, forKey: .accountType) ?? .user\n\n        // parse instance link\n        let instanceLink = try values.decode(URL.self, forKey: .instanceLink)\n        // Remove the \"api/v3\" path that we attached to the instanceLink pre-2.0\n        var components = URLComponents(url: instanceLink, resolvingAgainstBaseURL: false)!\n        // Adding a slash is important! The API returns instance actor IDs with a trailing slash.\n        components.path = \"/\"\n        guard let instanceLink = components.url else { throw DecodingError.cannotModifyPathComponents }\n        \n        guard instanceLink.host != nil,\n              let actorId = ActorIdentifier(url: instanceLink.appendingPathComponent(\"u/\\(name)\")) else {\n            throw DecodingError.invalidHost\n        }\n        self.actorId = actorId\n        \n        self.api = ApiClient.getApiClient(url: instanceLink, username: name)\n        do {\n            let keychain = Constants.main.keychain\n            let token = try keychain.get(getKeychainId(actorId: actorId)) ?? keychain.get(getKeychainId(id: id))\n            if let token {\n                api.updateToken(token)\n            } else {\n                handleError(DecodingError.noTokenInKeychain)\n            }\n        } catch {\n            handleError(error)\n        }\n        \n        self.description = try values.decodeIfPresent(String.self, forKey: .description)\n        self.banner = try values.decodeIfPresent(URL.self, forKey: .banner)\n        self.created = try values.decodeIfPresent(Date.self, forKey: .created)\n        self.updated = try values.decodeIfPresent(Date.self, forKey: .updated)\n    }\n    \n    func encode(to encoder: Encoder) throws {\n        saveTokenToKeychain()\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(id, forKey: .id)\n        try container.encode(name, forKey: .username)\n        try container.encode(storedNickname, forKey: .storedNickname)\n        try container.encode(siteSoftware, forKey: .siteSoftware)\n        try container.encode(avatar, forKey: .avatarUrl)\n        try container.encode(activityState, forKey: .activityState)\n        try container.encode(api.baseUrl, forKey: .instanceLink)\n        try container.encode(visitHistoryEnabled, forKey: .visitHistoryEnabled)\n        try container.encode(accountType, forKey: .accountType)\n        try container.encode(favorites, forKey: .favorites)\n        try container.encode(description, forKey: .description)\n        try container.encode(banner, forKey: .banner)\n        try container.encode(created, forKey: .created)\n        try container.encode(updated, forKey: .updated)\n    }\n    \n    var keychainId: String {\n        getKeychainId(actorId: actorId)\n    }\n    \n    @MainActor\n    func update(person: Person, software: SiteSoftware) {\n        var shouldSave = false\n        if avatar != person.avatar {\n            avatar = person.avatar\n            shouldSave = true\n        }\n        if siteSoftware != software {\n            siteSoftware = software\n            shouldSave = true\n        }\n        \n        let newAccountType: AccountType\n        if person.isAdmin.value_ ?? false {\n            newAccountType = .admin\n        } else if !(person.moderatedCommunities.value_?.isEmpty ?? true) {\n            newAccountType = .moderator\n        } else {\n            newAccountType = .user\n        }\n        if accountType != newAccountType {\n            accountType = newAccountType\n            shouldSave = true\n        }\n        if person.description != description {\n            description = person.description\n            shouldSave = true\n        }\n        if person.banner != banner {\n            banner = person.banner\n            shouldSave = true\n        }\n        if person.created != created {\n            created = person.created\n            shouldSave = true\n        }\n        if person.updated != updated {\n            updated = person.updated\n            shouldSave = true\n        }\n        \n        if shouldSave {\n            AccountsTracker.main.saveAccounts(ofType: .user)\n        }\n    }\n    \n    func updateToken(_ newToken: String) {\n        api.updateToken(newToken)\n    }\n    \n    func saveTokenToKeychain() {\n        if let token = api.token {\n            do {\n                try Constants.main.keychain.set(token, key: getKeychainId(actorId: actorId))\n            } catch {\n                handleError(error)\n            }\n        }\n    }\n    \n    func deleteTokenFromKeychain() {\n        try? Constants.main.keychain.remove(getKeychainId(actorId: actorId))\n        try? Constants.main.keychain.remove(getKeychainId(id: id))\n    }\n    \n    var isActive: Bool { AppState.main.activeSessions.contains(where: { $0 === self }) }\n    \n    var nicknameSortKey: String { nickname + actorId.host }\n    var instanceSortKey: String { actorId.host + nickname }\n    \n    var uniqueStringId: String {\n        assert(fullName != nil)\n        return fullName ?? \"\"\n    }\n    \n    var fullName: String? { \"\\(name)@\\(host)\" }\n    \n    var fullNameWithPrefix: String? { \"@\\(name)@\\(host)\" }\n    \n    func setNickname(_ newValue: String) {\n        storedNickname = newValue.isEmpty ? nil : newValue\n        AccountsTracker.main.saveAccounts(ofType: .user)\n    }\n    \n    var profileCreated: Date? { created }\n}\n\nprivate func getKeychainId(actorId: ActorIdentifier) -> String {\n    // localhost sometimes has url \"http://localhost:PORT\" and sometimes \"https://lemmy-alpha/beta/etc\" [1], so replace any of that with simple \"localhost\"\n    //\n    // [1](https://join-lemmy.org/docs/contributors/02-local-development.html#tests)\n\n    let keychainActorId = actorId.description.replacing(\n        /https?:\\/\\/(lemmy-(alpha|beta|gamma|delta|epsilon)|127\\.0\\.0\\.1:\\d{4}|localhost:\\d{4})/,\n        with: \"localhost\"\n    )\n    return \"\\(keychainActorId)_accessToken\"\n}\n\nprivate func getKeychainId(id: Int) -> String {\n    \"\\(id)_accessToken\"\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/Action.swift",
    "content": "//\n//  Action.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/03/2024.\n//\n\nimport SwiftUI\n\nprotocol Action: Identifiable {\n    var id: String { get }\n    var appearance: ActionAppearance { get }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/ActionAppearance+StaticValues.swift",
    "content": "//\n//  ActionAppearance+StaticValues.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/08/2024.\n//\n\nimport Foundation\n\nextension ActionAppearance {\n    static func upvote(isOn: Bool) -> Self {\n        .init(\n            label: isOn ? \"Undo Upvote\" : \"Upvote\",\n            isOn: isOn,\n            color: .themedUpvote,\n            icon: Icons.upvote,\n            menuIcon: isOn ? Icons.upvoteSquareFill : Icons.upvoteSquare,\n            swipeIcon1: isOn ? Icons.resetVoteSquare : Icons.upvoteSquare,\n            swipeIcon2: isOn ? Icons.resetVoteSquareFill : Icons.upvoteSquareFill\n        )\n    }\n    \n    static func downvote(isOn: Bool) -> Self {\n        .init(\n            label: isOn ? \"Undo Downvote\" : \"Downvote\",\n            isOn: isOn,\n            color: .themedDownvote,\n            icon: Icons.downvote,\n            menuIcon: isOn ? Icons.downvoteSquareFill : Icons.downvoteSquare,\n            swipeIcon1: isOn ? Icons.resetVoteSquare : Icons.downvoteSquare,\n            swipeIcon2: isOn ? Icons.resetVoteSquareFill : Icons.downvoteSquareFill\n        )\n    }\n    \n    static func save(isOn: Bool) -> Self {\n        .init(\n            label: isOn ? \"Unsave\" : \"Save\",\n            isOn: isOn,\n            color: .themedSave,\n            icon: isOn ? Icons.saveFill : Icons.save,\n            swipeIcon1: isOn ? Icons.unsave : Icons.save,\n            swipeIcon2: isOn ? Icons.unsaveFill : Icons.saveFill\n        )\n    }\n    \n    static func createImage() -> Self {\n        .init(\n            label: \"Create Image\",\n            color: .themedAccent,\n            icon: Icons.createImage\n        )\n    }\n    \n    static func reply() -> Self {\n        .init(\n            label: \"Reply\",\n            color: .themedAccent,\n            icon: Icons.reply,\n            swipeIcon2: Icons.replyFill\n        )\n    }\n    \n    static func blockCreator() -> Self {\n        .init(\n            label: \"Block User\",\n            isOn: false,\n            isDestructive: true,\n            color: .themedNegative,\n            icon: Icons.block,\n            swipeIcon2: Icons.blockFill\n        )\n    }\n    \n    static func banFromInstance(isOn: Bool, withUserLabel: Bool = false) -> Self {\n        .init(\n            label: getBanLabel(isOn: isOn, withUserLabel: withUserLabel),\n            isOn: isOn,\n            isDestructive: !isOn,\n            color: isOn ? .themedPositive : .themedNegative,\n            icon: isOn ? Icons.unbanFromInstance : Icons.banFromInstance,\n            swipeIcon2: isOn ? Icons.unbanFromInstanceFill : Icons.banFromInstanceFill\n        )\n    }\n    \n    static func banFromCommunity(isOn: Bool, withUserLabel: Bool = false) -> Self {\n        .init(\n            label: getBanLabel(isOn: isOn, withUserLabel: withUserLabel),\n            isOn: isOn,\n            isDestructive: !isOn,\n            color: isOn ? .themedPositive : .themedNegative,\n            icon: isOn ? Icons.unbanFromCommunity : Icons.banFromCommunity,\n            swipeIcon2: isOn ? Icons.unbanFromCommunityFill : Icons.banFromCommunityFill\n        )\n    }\n    \n    private static func getBanLabel(isOn: Bool, withUserLabel: Bool) -> LocalizedStringResource {\n        if withUserLabel {\n            isOn ? \"Unban User\" : \"Ban User\"\n        } else {\n            isOn ? \"Unban\" : \"Ban\"\n        }\n    }\n    \n    static func block(isOn: Bool) -> Self {\n        .init(\n            label: isOn ? \"Unblock\" : \"Block\",\n            isOn: isOn,\n            isDestructive: !isOn,\n            color: .themedNegative,\n            icon: isOn ? Icons.unblock : Icons.block,\n            swipeIcon2: isOn ? Icons.unblockFill : Icons.blockFill\n        )\n    }\n    \n    static func hide(isOn: Bool) -> Self {\n        .init(\n            label: isOn ? \"Show\" : \"Hide\",\n            isOn: isOn,\n            color: .themedNeutralAccent,\n            icon: isOn ? Icons.show : Icons.hide\n        )\n    }\n    \n    static func selectText() -> Self {\n        .init(\n            label: \"Select Text\",\n            isOn: false,\n            color: .themedAccent,\n            icon: Icons.select\n        )\n    }\n    \n    static func share() -> Self {\n        .init(\n            label: \"Share...\",\n            color: .themedNeutralAccent,\n            icon: Icons.share\n        )\n    }\n    \n    static func report() -> Self {\n        .init(\n            label: \"Report\",\n            isOn: false,\n            isDestructive: true,\n            color: .themedNegative,\n            icon: Icons.moderationReport,\n            swipeIcon2: Icons.moderationReportFill\n        )\n    }\n    \n    static func markRead(isOn: Bool) -> Self {\n        .init(\n            label: isOn ? \"Mark Unread\" : \"Mark Read\",\n            isOn: isOn,\n            color: .themedRead,\n            icon: isOn ? Icons.markUnread : Icons.markRead,\n            swipeIcon1: isOn ? Icons.markRead : Icons.markUnread,\n            swipeIcon2: isOn ? Icons.markUnreadFill : Icons.markReadFill\n        )\n    }\n    \n    static func edit() -> Self {\n        .init(label: \"Edit\", color: .themedAccent, icon: Icons.edit)\n    }\n    \n    static func pin(isOn: Bool, isInProgress: Bool = false) -> Self {\n        .init(\n            label: isOn ? \"Unpin\" : \"Pin\",\n            isOn: isOn,\n            isInProgress: isInProgress,\n            color: .themedModeration,\n            icon: isOn ? Icons.unpin : Icons.pin,\n            barIcon: isOn ? Icons.pinFill : Icons.pin,\n            swipeIcon2: isOn ? Icons.unpinFill : Icons.pinFill\n        )\n    }\n    \n    static func pinToCommunity(isOn: Bool, isInProgress: Bool = false) -> Self {\n        .init(\n            label: isOn ? \"Unpin From Community\" : \"Pin to Community\",\n            isOn: isOn,\n            isInProgress: isInProgress,\n            color: .themedModeration,\n            icon: isOn ? Icons.unpin : Icons.pin,\n            barIcon: isOn ? Icons.pinFill : Icons.pin,\n            swipeIcon2: isOn ? Icons.unpinFill : Icons.pinFill\n        )\n    }\n    \n    static func pinToInstance(isOn: Bool, isInProgress: Bool = false) -> Self {\n        .init(\n            label: isOn ? \"Unpin From Instance\" : \"Pin to Instance\",\n            isOn: isOn,\n            isInProgress: isInProgress,\n            color: .themedAdministration,\n            icon: isOn ? Icons.unpin : Icons.pin,\n            barIcon: isOn ? Icons.pinFill : Icons.pin,\n            swipeIcon2: isOn ? Icons.unpinFill : Icons.pinFill\n        )\n    }\n    \n    static func lock(isOn: Bool, isInProgress: Bool = false) -> Self {\n        .init(\n            label: isOn ? \"Unlock\" : \"Lock\",\n            isOn: isOn,\n            isInProgress: isInProgress,\n            color: .themedLockAccent,\n            icon: isOn ? Icons.unlock : Icons.lock,\n            barIcon: isOn ? Icons.lockFill : Icons.lock,\n            swipeIcon2: isOn ? Icons.unlockFill : Icons.lockFill\n        )\n    }\n    \n    static func remove(isOn: Bool, isInProgress: Bool = false) -> Self {\n        .init(\n            label: isOn ? \"Restore\" : \"Remove\",\n            isOn: isOn,\n            isInProgress: isInProgress,\n            isDestructive: !isOn,\n            color: isOn ? .themedPositive : .themedNegative,\n            icon: isOn ? Icons.restore : Icons.remove,\n            swipeIcon2: isOn ? Icons.restoreFill : Icons.removeFill\n        )\n    }\n\n    static func toggleNsfw(isOn: Bool) -> Self {\n        .init(\n            label: isOn ? \"Remove NSFW Tag\" : \"Add NSFW Tag\",\n            color: .themedNegative,\n            icon: Icons.blurNsfw\n        )\n    }\n    \n    static func resolve(isOn: Bool) -> Self {\n        .init(\n            label: isOn ? \"Unresolve\" : \"Resolve\",\n            isOn: isOn,\n            color: .themedPositive,\n            icon: isOn ? Icons.unresolve : Icons.resolve,\n            barIcon: isOn ? Icons.resolveFill : Icons.resolve,\n            swipeIcon2: isOn ? Icons.unresolveFill : Icons.resolveFill\n        )\n    }\n    \n    /// Adds or removes a user as administrator\n    /// - Parameter isOn: true when user is admin, false otherwise\n    static func addAdmin(isOn: Bool) -> Self {\n        .init(\n            label: isOn ? \"Remove Administrator\" : \"Appoint Administrator\",\n            isDestructive: isOn,\n            color: isOn ? .themedNegative : .themedPositive,\n            icon: isOn ? Icons.removeAdministrator : Icons.administration,\n            swipeIcon1: isOn ? Icons.removeAdministrator : Icons.administration,\n            swipeIcon2: isOn ? Icons.removeAdministratorFill : Icons.administrationFill\n        )\n    }\n    \n    /// Adds or removes a user as moderator\n    /// - Parameter isOn: true when user is moderator, false otherwise\n    static func addMod(isOn: Bool) -> Self {\n        .init(\n            label: isOn ? \"Remove Moderator\" : \"Appoint Moderator\",\n            color: isOn ? .themedNegative : .themedPositive,\n            icon: isOn ? Icons.demoteModerator : Icons.moderation,\n            swipeIcon1: isOn ? Icons.demoteModerator : Icons.moderation,\n            swipeIcon2: isOn ? Icons.demoteModeratorFill : Icons.moderationFill\n        )\n    }\n    \n    static func purge(isInProgress: Bool = false) -> Self {\n        .init(\n            label: \"Purge\",\n            isInProgress: isInProgress,\n            isDestructive: true,\n            color: .themedWarning,\n            icon: Icons.purge\n        )\n    }\n    \n    static func purgePerson(isInProgress: Bool = false) -> Self {\n        .init(\n            label: \"Purge User\",\n            isInProgress: isInProgress,\n            isDestructive: true,\n            color: .themedWarning,\n            icon: Icons.purge\n        )\n    }\n    \n    static func crossPost() -> Self {\n        .init(label: \"Crosspost\", color: .themedAccent, icon: Icons.crossPost)\n    }\n    \n    static func viewVotes() -> Self {\n        .init(label: \"View Votes\", color: .themedAccent, icon: Icons.votes)\n    }\n        \n    static func collapse() -> Self {\n        .init(\n            label: \"Collapse\",\n            color: .themedColorfulAccent(4),\n            icon: Icons.collapse,\n            swipeIcon1: Icons.collapseSquare,\n            swipeIcon2: Icons.collapseSquareFill\n        )\n    }\n    \n    static func collapseParent() -> Self {\n        .init(\n            label: \"Collapse Parent\",\n            color: .themedColorfulAccent(4),\n            icon: Icons.collapseParent,\n            swipeIcon1: Icons.collapseParentSquare,\n            swipeIcon2: Icons.collapseParentSquareFill\n        )\n    }\n    \n    static func collapseToTop() -> Self {\n        .init(\n            label: \"Collapse to Top\",\n            color: .themedColorfulAccent(4),\n            icon: Icons.collapseToTop,\n            swipeIcon1: Icons.collapseToTopSquare,\n            swipeIcon2: Icons.collapseToTopSquareFill\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/ActionAppearance.swift",
    "content": "//\n//  ActionAppearance.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/08/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct ActionAppearance {\n    let label: String\n    let isOn: Bool\n    let isInProgress: Bool\n    let isDestructive: Bool\n    let color: ThemedColor\n    let barIcon: String\n    let menuIcon: String\n    let swipeIcon1: String\n    let swipeIcon2: String\n    \n    init(\n        label: LocalizedStringResource,\n        isOn: Bool = false,\n        isInProgress: Bool = false,\n        isDestructive: Bool = false,\n        color: ThemedColor,\n        icon: String,\n        barIcon: String? = nil,\n        menuIcon: String? = nil,\n        swipeIcon1: String? = nil,\n        swipeIcon2: String? = nil\n    ) {\n        self.init(\n            label: .init(localized: label),\n            isOn: isOn,\n            isInProgress: isInProgress,\n            isDestructive: isDestructive,\n            color: color,\n            icon: icon,\n            barIcon: barIcon,\n            menuIcon: menuIcon,\n            swipeIcon1: swipeIcon1,\n            swipeIcon2: swipeIcon2\n        )\n    }\n    \n    @_disfavoredOverload\n    init(\n        label: String,\n        isOn: Bool = false,\n        isInProgress: Bool = false,\n        isDestructive: Bool = false,\n        color: ThemedColor,\n        icon: String,\n        barIcon: String? = nil,\n        menuIcon: String? = nil,\n        swipeIcon1: String? = nil,\n        swipeIcon2: String? = nil\n    ) {\n        self.label = label\n        self.isOn = isOn\n        self.isInProgress = isInProgress\n        self.isDestructive = isDestructive\n        self.color = color\n        self.barIcon = barIcon ?? icon\n        self.menuIcon = menuIcon ?? icon\n        self.swipeIcon1 = swipeIcon1 ?? icon\n        self.swipeIcon2 = swipeIcon2 ?? icon\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/ActionBuilder.swift",
    "content": "//\n//  ActionGroupBuilder.swift\n//  Mlem\n//\n//  Created by Sjmarf on 07/07/2024.\n//\n\nimport Foundation\n\n@resultBuilder\nstruct ActionBuilder {\n    static func buildBlock(_ children: [any Action]...) -> [any Action] {\n        children.flatMap { $0 }\n    }\n\n    static func buildEither(first: [any Action]) -> [any Action] {\n        first\n    }\n\n    static func buildEither(second: [any Action]) -> [any Action] {\n        second\n    }\n    \n    static func buildExpression(_ expression: any Action) -> [any Action] {\n        [expression]\n    }\n\n    static func buildExpression(_ expression: [any Action]) -> [any Action] {\n        expression\n    }\n    \n    static func buildOptional(_ action: [any Action]?) -> [any Action] {\n        if let action { return action }\n        return []\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/ActionGroup.swift",
    "content": "//\n//  GroupAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 31/03/2024.\n//\n\nimport SwiftUI\n\nstruct ActionGroup: Action {\n    enum DisplayMode {\n        case section, compactSection, disclosure, popup\n    }\n    \n    let id: String = UUID().uuidString\n    let appearance: ActionAppearance\n\n    let prompt: String?\n    \n    let disabled: Bool\n    let children: [any Action]\n    \n    /// Represents how the children of the `ActionGroup` are presented.\n    let displayMode: DisplayMode\n    \n    init(\n        appearance: ActionAppearance = .groupDefault,\n        prompt: LocalizedStringResource? = nil,\n        disabled: Bool? = nil,\n        displayMode: DisplayMode = .section,\n        @ActionBuilder children: () -> [any Action]\n    ) {\n        let stringPrompt: String?\n        if let prompt {\n            stringPrompt = .init(localized: prompt)\n        } else {\n            stringPrompt = nil\n        }\n        self.init(\n            appearance: appearance,\n            prompt: stringPrompt,\n            disabled: disabled,\n            displayMode: displayMode,\n            children: children\n        )\n    }\n    \n    @_disfavoredOverload\n    init(\n        appearance: ActionAppearance = .groupDefault,\n        prompt: String? = nil,\n        disabled: Bool? = nil,\n        displayMode: DisplayMode = .section,\n        @ActionBuilder children: () -> [any Action]\n    ) {\n        self.appearance = appearance\n        self.prompt = prompt\n        let children = children()\n        self.disabled = disabled ?? !children.allSatisfy { action in\n            if let action = action as? BasicAction {\n                return !action.disabled\n            } else if let action = action as? ActionGroup {\n                return !action.disabled\n            }\n            return true\n        }\n        self.children = children\n        self.displayMode = displayMode\n    }\n}\n\nprivate extension ActionAppearance {\n    static let groupDefault: Self = .init(label: \"More...\", color: .themedNeutralAccent, icon: Icons.menuCircle)\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/ActionType.swift",
    "content": "//\n//  ActionType.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/03/2024.\n//\n\nimport SwiftUI\n\nenum ActionType: String {\n    case upvote, downvote, save\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/BasicAction.swift",
    "content": "//\n//  BasicAction.swift\n//  Mlem\n//\n//  Created by Sjmarf on 31/03/2024.\n//\n\nimport Dependencies\nimport MlemMiddleware\nimport SwiftUI\n\nstruct BasicAction: Action {\n    let id: String\n    let appearance: ActionAppearance\n    \n    let confirmationPrompt: String?\n\n    /// If this is nil, the BasicAction is disabled\n    var callback: (@MainActor () -> Void)?\n    \n    var disabled: Bool { callback == nil }\n    \n    /// - Parameter id: This must be unique to the action AND contain the model's unique ID.\n    /// If you don't do this, SwiftUI can get confused in a lazy view.\n    init(\n        id: String,\n        appearance: ActionAppearance,\n        confirmationPrompt: LocalizedStringResource? = nil,\n        enabled: Bool = true,\n        callback: (@MainActor () -> Void)? = nil\n    ) {\n        self.id = id\n        self.appearance = appearance\n        if let confirmationPrompt {\n            self.confirmationPrompt = .init(localized: confirmationPrompt)\n        } else {\n            self.confirmationPrompt = nil\n        }\n        self.callback = enabled ? callback : nil\n    }\n    \n    @MainActor\n    func callbackWithConfirmation(popupModel: PopupAnchorModel) {\n        if let callback {\n            if let confirmationPrompt {\n                popupModel.showPopup(ActionGroup(\n                    appearance: .init(label: \"Confirm\", color: .themedNeutralAccent, icon: Icons.success),\n                    prompt: confirmationPrompt,\n                    children: {\n                        BasicAction(\n                            id: \"\",\n                            appearance: .init(\n                                label: \"Yes\",\n                                isOn: false,\n                                isDestructive: true,\n                                color: .themedWarning,\n                                icon: \"\"\n                            ),\n                            callback: callback\n                        )\n                    }\n                ))\n            } else {\n                callback()\n            }\n        }\n    }\n    \n    func disabled(_ value: Bool) -> BasicAction {\n        var new = self\n        if value {\n            new.callback = nil\n        }\n        return new\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/Counter.swift",
    "content": "//\n//  Counter.swift\n//  Mlem\n//\n//  Created by Sjmarf on 14/06/2024.\n//\n\nimport Foundation\n\nstruct Counter: Identifiable {\n    let id: UUID = .init()\n    let value: Int?\n    \n    let leadingAction: (any Action)?\n    let trailingAction: (any Action)?\n    \n    var appearance: CounterAppearance {\n        .init(\n            value: value,\n            leading: leadingAction?.appearance,\n            trailing: trailingAction?.appearance,\n            label: \"Unknown\",\n            singleIcon: \"\"\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/CounterAppearance.swift",
    "content": "//\n//  CounterAppearance.swift\n//  Mlem\n//\n//  Created by Sjmarf on 17/08/2024.\n//\n\nimport Foundation\n\nstruct CounterAppearance {\n    let value: Int?\n    let leading: ActionAppearance?\n    let trailing: ActionAppearance?\n    let label: LocalizedStringResource\n    let singleIcon: String\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/CounterApperance+StaticValues.swift",
    "content": "//\n//  CounterApperance+StaticValues.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-01-29.\n//\n\nextension CounterAppearance {\n    static func score(value: Int = 7, upvoteOn: Bool = false, downvoteOn: Bool = false) -> CounterAppearance {\n        .init(\n            value: value,\n            leading: .upvote(isOn: upvoteOn),\n            trailing: .downvote(isOn: downvoteOn),\n            label: \"Score Counter\",\n            singleIcon: Icons.scoreCounter\n        )\n    }\n    \n    static func upvote(value: Int = 9, isOn: Bool = false) -> CounterAppearance {\n        .init(value: value, leading: .upvote(isOn: isOn), trailing: nil, label: \"Upvote Counter\", singleIcon: Icons.upvoteCounter)\n    }\n    \n    static func downvote(value: Int = 2, isOn: Bool = false) -> CounterAppearance {\n        .init(value: value, leading: .downvote(isOn: isOn), trailing: nil, label: \"Downvote Counter\", singleIcon: Icons.downvoteCounter)\n    }\n    \n    static func reply(value: Int = 3) -> CounterAppearance {\n        .init(value: value, leading: .reply(), trailing: nil, label: \"Reply Counter\", singleIcon: Icons.replyCounter)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/Readout.swift",
    "content": "//\n//  Readout.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/06/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct Readout {\n    let id: String\n    let label: String?\n    let icon: String\n    var color: ThemedColor?\n    var value: String?\n    var valueColor: ThemedColor?\n}\n"
  },
  {
    "path": "Mlem/App/Models/Action/ShareActivity.swift",
    "content": "//\n//  ShareActivity.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/09/2024.\n//\n\nimport UIKit\n\nclass ShareActivity: UIActivity {\n    let appearance: ActionAppearance\n    let action: @MainActor () -> Void\n    \n    init(appearance: ActionAppearance, performAction: @escaping @MainActor () -> Void) {\n        self.appearance = appearance\n        self.action = performAction\n        super.init()\n    }\n    \n    override var activityTitle: String? {\n        appearance.label\n    }\n\n    override var activityImage: UIImage? {\n        .init(systemName: appearance.menuIcon)\n    }\n    \n    override var activityType: UIActivity.ActivityType {\n        UIActivity.ActivityType(rawValue: \"com.hanners.mlem\")\n    }\n\n    override class var activityCategory: UIActivity.Category {\n        .action\n    }\n    \n    override func canPerform(withActivityItems activityItems: [Any]) -> Bool {\n        true\n    }\n    \n    @MainActor\n    override func perform() {\n        action()\n        activityDidFinish(true)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/CommentTreeNode.swift",
    "content": "//\n//  CommentWrapper.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/06/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\n@Observable\nclass CommentTreeNode: Identifiable, Hashable {\n    var comment: Comment\n    private(set) var children: [CommentTreeNode] = []\n    weak var parent: CommentTreeNode?\n    var collapsed: Bool = false\n    \n    var id: Int { comment.id }\n    \n    init(_ comment: Comment) {\n        self.comment = comment\n    }\n    \n    func addChild(_ child: CommentTreeNode) {\n        child.parent = self\n        children.append(child)\n    }\n    \n    func tree(hideIfCollapsed: Bool = true) -> [CommentTreeNode] {\n        if comment.creator.value_?.blocked_.realizedValue ?? false { return [] }\n        if collapsed, hideIfCollapsed { return [self] }\n        return children.reduce([self]) { $0 + $1.tree() }\n    }\n    \n    var recursiveChildCount: Int {\n        children.reduce(0) { $0 + $1.recursiveChildCount + 1 }\n    }\n    \n    var api: ApiClient { comment.api }\n    var actorId: ActorIdentifier { comment.actorId }\n    \n    /// Returns the top-level parent\n    var topParent: CommentTreeNode { parent?.topParent ?? self }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(ObjectIdentifier(self))\n    }\n    \n    static func == (lhs: CommentTreeNode, rhs: CommentTreeNode) -> Bool { lhs === rhs }\n}\n\nextension [CommentTreeNode] {\n    func tree() -> [CommentTreeNode] {\n        reduce([]) { $0 + $1.tree() }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/ErrorDetails.swift",
    "content": "//\n//  ErrorDetails.swift\n//  Mlem\n//\n//  Created by Sjmarf on 31/08/2023.\n//\n\nimport Combine\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport UniformTypeIdentifiers\n\nstruct ErrorDetails: Hashable {\n    var title: String?\n    var body: String?\n    var error: Error?\n    var location: String?\n    var icon: Icon?\n    var buttonText: String?\n    var refresh: (() async -> Bool)?\n    var autoRefresh: Bool = false\n    var when: Date\n    \n    init(\n        title: String? = nil,\n        body: String? = nil,\n        error: Error? = nil,\n        location: String? = nil,\n        icon: Icon? = nil,\n        buttonText: String? = nil,\n        refresh: (() async -> Bool)? = nil,\n        autoRefresh: Bool = false\n    ) {\n        self.title = title\n        self.body = body\n        self.error = error\n        self.location = location\n        self.icon = icon\n        self.buttonText = buttonText\n        self.refresh = refresh\n        self.autoRefresh = autoRefresh\n        self.when = Date.now\n        \n        if let error {\n            switch error {\n            case ApiClientError.imageTooLarge:\n                self.title = self.title ?? \"Image too large\"\n            default:\n                break\n            }\n        }\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(title)\n        hasher.combine(body)\n        hasher.combine(error?.localizedDescription)\n        hasher.combine(location)\n        hasher.combine(icon)\n        hasher.combine(buttonText)\n        hasher.combine(refresh == nil)\n        hasher.combine(autoRefresh)\n    }\n\n    static func == (lhs: ErrorDetails, rhs: ErrorDetails) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n    \n    func errorText(includingLocation: Bool = true) -> String {\n        var output = String(describing: error)\n        if includingLocation, let location {\n            output += \" (\\(location))\"\n        }\n        for account in AccountsTracker.main.userAccounts {\n            if let token = account.api.token {\n                output.replace(token, with: \"TOKEN_REDACTED\")\n            }\n        }\n        return output\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Events/Event+Extension.swift",
    "content": "//\n//  Event+Extension.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-24.\n//\n\nimport Foundation\nimport FediverseEvents\n\nextension Event {\n    var navigationUrl: URL? {\n        if let social = self.social.first(where: { $0.icon == .lemmy }) {\n            return social.url\n        } else {\n            return self.endpoints.open\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Events/EventsTracker.swift",
    "content": "//\n//  EventsTracker.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-23.\n//\n\nimport Foundation\nimport FediverseEvents\n\n@Observable\nclass EventsTracker {\n    private let client = EventsClient()\n\n    private(set) var events: [Event]?\n    private var lastRefreshedAt: Date?\n\n    var environment: EventsEnvironment { client.environment }\n\n    func changeEnvironment(to environment: EventsEnvironment) {\n        self.client.changeEnvironment(to: environment)\n        self.lastRefreshedAt = nil\n        self.events = nil\n        self.refreshIfStale()\n    }\n\n    private func refresh() async throws {\n        self.events = try await self.client.listEvents()\n        self.lastRefreshedAt = .now\n    }\n\n    func refreshIfStale() {\n        if self.needsRefresh {\n            Task {\n                do {\n                    try await refresh()\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n    }\n\n    private var needsRefresh: Bool {\n        if let lastRefreshedAt {\n            abs(lastRefreshedAt.timeIntervalSinceNow) > 60 * 60 // 1 hour\n        } else {\n            true\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/FeedContext.swift",
    "content": "//\n//  FeedContext.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-27.\n//\n\nimport Foundation\n\nenum FeedContext {\n    case all, local, subscribed, saved, moderated, popular, suggested, community, search, person, post\n\n    var showSubscriptionIndicator: Bool {\n        switch self {\n        case .all, .local, .popular, .suggested, .saved, .search, .person, .post:\n            return true\n        default:\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/FeedbackType.swift",
    "content": "//\n//  FeedbackType.swift\n//  Mlem\n//\n//  Created by Sjmarf on 24/06/2024.\n//\n\nimport Foundation\n\nenum FeedbackType {\n    case haptic\n    case toast\n}\n"
  },
  {
    "path": "Mlem/App/Models/ImageUploadHistoryManager.swift",
    "content": "//\n//  ImageUploadHistoryManager.swift\n//  Mlem\n//\n//  Created by Sjmarf on 07/09/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\n@Observable\nclass ImageUploadHistoryManager {\n    private(set) var uploads: [ImageUpload1] = []\n    \n    func add(_ upload: ImageUpload1) {\n        uploads.append(upload)\n    }\n    \n    func deleteAll() {\n        for upload in uploads {\n            Task { try await upload.delete() }\n        }\n    }\n    \n    @discardableResult\n    func deleteWhereNotPresent(in text: String) -> [ImageUpload1] {\n        uploads.filter { upload in\n            if !text.contains(upload.url.absoluteString) {\n                Task { try await upload.delete() }\n                return true\n            }\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/ImageUploadManager.swift",
    "content": "//\n//  ImageUploadManager.swift\n//  Mlem\n//\n//  Created by Sjmarf on 02/09/2024.\n//\n\nimport MlemMiddleware\nimport PhotosUI\nimport SwiftUI\n\n@Observable\nclass ImageUploadManager: Hashable {\n    enum UploadState: Hashable {\n        case idle, uploading(progress: Double), done(ImageUpload1)\n        \n        var isDone: Bool {\n            switch self {\n            case .done: true\n            default: false\n            }\n        }\n    }\n    \n    private(set) var state: UploadState = .idle\n    \n    init() {}\n    \n    var image: ImageUpload1? {\n        switch state {\n        case let .done(image):\n            return image\n        default:\n            return nil\n        }\n    }\n    \n    var progress: Double {\n        switch state {\n        case .idle: 0\n        case let .uploading(progress): progress\n        case .done: 1\n        }\n    }\n    \n    func uploadPhoto(_ photo: PhotosPickerItem, api: ApiClient) async throws {\n        do {\n            guard let data = try await photo.loadTransferable(type: Data.self) else {\n                throw ApiClientError.unsuccessful\n            }\n            guard let fileExtension = photo.supportedContentTypes.first?.preferredFilenameExtension else {\n                throw ApiClientError.unsuccessful\n            }\n            try await upload(data: data, fileExtension: fileExtension, api: api)\n        } catch {\n            Task { @MainActor in\n                state = .idle\n            }\n            throw error\n        }\n    }\n    \n    func uploadFile(localUrl url: URL, api: ApiClient) async throws {\n        do {\n            guard url.startAccessingSecurityScopedResource() else {\n                throw ApiClientError.insufficientPermissions\n            }\n            let data = try Data(contentsOf: url)\n            url.stopAccessingSecurityScopedResource()\n            try await upload(data: data, fileExtension: url.pathExtension, api: api)\n            \n        } catch {\n            url.stopAccessingSecurityScopedResource()\n            Task { @MainActor in\n                state = .idle\n            }\n            throw error\n        }\n    }\n    \n    func pasteFromClipboard(api: ApiClient) async throws {\n        do {\n            if UIPasteboard.general.hasImages, let content = UIPasteboard.general.image {\n                if let data = content.pngData() {\n                    try await upload(data: data, fileExtension: \"png\", api: api)\n                }\n            }\n        } catch {\n            Task { @MainActor in\n                state = .idle\n            }\n            throw error\n        }\n    }\n    \n    func upload(data: Data, fileExtension: String, api: ApiClient) async throws {\n        do {\n            let image = try await api.uploadImage(data, fileExtension: fileExtension, onProgress: { value in\n                Task { @MainActor in\n                    self.state = .uploading(progress: value)\n                }\n            })\n            Task { @MainActor in\n                state = .done(image)\n            }\n        } catch {\n            Task { @MainActor in\n                state = .idle\n            }\n            throw error\n        }\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(state)\n    }\n    \n    @MainActor\n    func clear() {\n        state = .idle\n    }\n    \n    func delete() async throws {\n        var imageToDelete: ImageUpload1?\n        if let image {\n            imageToDelete = image\n        }\n        await clear()\n        try await imageToDelete?.delete()\n    }\n    \n    static func == (lhs: ImageUploadManager, rhs: ImageUploadManager) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/MlemStats/InstanceSummary.swift",
    "content": "//\n//  InstanceSummary.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/06/2024.\n//\n\nimport Foundation\nimport MlemBackend\nimport MlemMiddleware\n\npublic extension InstanceSummary {\n    var instanceStub: InstanceStub {\n        .init(api: AppState.main.firstApi, actorId: .instance(host: host))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/MlemStats/MlemStats.swift",
    "content": "//\n//  MlemStats.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/06/2024.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport MlemBackend\n\n/// Class exposing instance search functionality. Instance data is fetched from the Mlem backend.\nclass MlemStats {\n    enum MlemStatsApiClientError: Error { case failed }\n    private(set) var instances: [InstanceSummary]?\n\n    private(set) var loadingState: LoadingState = .idle\n    private(set) var errorDetails: ErrorDetails?\n    \n    // This set is queried for use in link-handling.\n    // Some of the largest instances are hard-coded just in-case the backend is down.\n    private(set) var hosts: Set<String> = [\n        \"lemm.ee\",\n        \"lemmy.world\",\n        \"lemmy.ml\",\n        \"sh.itjust.works\",\n        \"beehaw.org\",\n        \"lemmy.blahaj.zone\",\n        \"sopuli.xyz\",\n        \"programming.dev\"\n    ]\n    \n    static let main: MlemStats = .init()\n    \n    @MainActor\n    func loadInstances(forceRefresh: Bool = false) async {\n        guard forceRefresh || loadingState == .idle else { return }\n        loadingState = .loading\n        do {\n            let decoder: JSONDecoder = .defaultDecoder\n            decoder.keyDecodingStrategy = .convertFromSnakeCase\n            let instances = try await BackendClient.main.getInstances()\n            self.instances = instances\n            hosts.formUnion(Set(instances.lazy.map(\\.host)))\n            loadingState = .done\n            errorDetails = nil\n        } catch {\n            loadingState = .idle\n            if var errorDetails = handleErrorWithDetails(error) {\n                errorDetails.refresh = {\n                    await self.loadInstances()\n                    return true\n                }\n                self.errorDetails = errorDetails\n            }\n        }\n    }\n    \n    @MainActor\n    func searchInstances(query: String, sort: InstanceSort = .score) async throws -> [InstanceSummary] {\n        await loadInstances()\n        let instances: [InstanceSummary]\n        if query.isEmpty {\n            instances = self.instances ?? []\n        } else {\n            instances = self.instances?.filter {\n                $0.host.localizedCaseInsensitiveContains(query)\n                    || $0.name.localizedCaseInsensitiveContains(query)\n            } ?? []\n        }\n        \n        let filteredInstances = filterBlockedInstances(instances)\n        \n        switch sort {\n        case .score:\n            return filteredInstances\n        case .users:\n            return filteredInstances.sorted { $0.totalUsers > $1.totalUsers }\n        case .alphabetical:\n            return filteredInstances.sorted { $0.host < $1.host }\n        case .version:\n            return filteredInstances.sorted { $0.software.version > $1.software.version }\n        }\n    }\n    \n    private func filterBlockedInstances(_ instances: [InstanceSummary]) -> [InstanceSummary] {\n        guard let session = AppState.main.firstSession as? UserSession, let blocks = session.blocks else {\n            return instances\n        }\n        \n        return instances.filter { instance in\n            let actorId = ActorIdentifier.instance(host: instance.host)\n            return !blocks.contains(instanceActorId: actorId)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/SeededRandomNumberGenerator.swift",
    "content": "//\n//  SeededRandomNumberGenerator.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-02.\n//\n\nimport Foundation\n\n// https://stackoverflow.com/questions/54821659/swift-4-2-seeding-a-random-number-generator\nstruct SeededRandomNumberGenerator: RandomNumberGenerator {\n    init(seed: Int) { srand48(seed) }\n    // swiftlint:disable:next legacy_random\n    func next() -> UInt64 { UInt64(drand48() * Double(UInt64.max)) }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Session/GuestSession.swift",
    "content": "//\n//  GuestSession.swift\n//  Mlem\n//\n//  Created by Sjmarf on 24/05/2024.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport Observation\n\n@Observable\nclass GuestSession: Session {\n    typealias AccountType = GuestAccount\n    \n    private(set) var account: GuestAccount\n    private(set) var instance: Instance?\n\n    init(account: GuestAccount) {\n        self.account = account\n        account.activate()\n        \n        Task {\n            let instance = try await self.api.getMyInstance()\n            let software = try await self.api.software\n            await self.account.update(instance: instance, software: software)\n            self.instance = instance\n        }\n    }\n    \n    convenience init(url: URL) throws {\n        try self.init(account: .getGuestAccount(url: url))\n    }\n    \n    func deactivate() {\n        account.deactivate()\n        api.cleanCaches()\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(actorId)\n    }\n    \n    static func == (lhs: GuestSession, rhs: GuestSession) -> Bool {\n        lhs.actorId == rhs.actorId\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Session/Session.swift",
    "content": "//\n//  Session.swift\n//  Mlem\n//\n//  Created by Sjmarf on 24/05/2024.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport Observation\n\nprotocol Session: ActorIdentifiable, Hashable {\n    associatedtype AccountType: Account\n    \n    var api: ApiClient { get }\n    var account: AccountType { get }\n    var instance: Instance? { get }\n    \n    func deactivate()\n}\n\nextension Session {\n    var api: ApiClient { account.api }\n    var actorId: ActorIdentifier { account.actorId }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Session/UserSession.swift",
    "content": "//\n//  UserSession.swift\n//  Mlem\n//\n//  Created by Sjmarf on 24/05/2024.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport Observation\n\n@Observable\nclass UserSession: Session {\n    typealias AccountType = UserAccount\n    \n    private(set) var account: UserAccount\n    \n    private(set) var person: Person?\n    private(set) var instance: Instance?\n    private(set) var subscriptions: SubscriptionList!\n    private(set) var blocks: BlockList?\n    private(set) var unreadCount: UnreadCount?\n    /// This **only** includes requests made by calling `toggleInstanceBlock` on this `UserSession`.\n    private(set) var ongoingInstanceBlockRequests: Set<ActorIdentifier> = []\n    private(set) var visitHistory: VisitHistory?\n    \n    private(set) var subscriptionListErrorDetails: ErrorDetails?\n\n    init(account: UserAccount) {\n        self.account = account\n        account.activate()\n        self.subscriptions = api.setupSubscriptionList(\n            getFavorites: { account.favorites },\n            setFavorites: {\n                account.favorites = $0\n                AccountsTracker.main.saveAccounts(ofType: .user)\n            }\n        )\n        \n        Task { @MainActor in\n            do {\n                let (person, instance, blocks) = try await self.api.getMyPerson()\n                let software = try await self.api.software\n                if let person {\n                    self.account.update(person: person, software: software)\n                    self.person = person\n                }\n                self.blocks = blocks\n                self.instance = instance\n            } catch {\n                handleError(error)\n            }\n            \n            do {\n                self.unreadCount = try await api.getUnreadCount()\n            } catch {\n                handleError(error)\n            }\n            \n            do {\n                try await self.api.getSubscriptionList()\n            } catch {\n                self.subscriptionListErrorDetails = handleErrorWithDetails(error)\n            }\n            \n            if account.visitHistoryEnabled {\n                do {\n                    self.visitHistory = try await PersistenceRepository.liveValue.loadVisitHistory(for: account)\n                } catch {\n                    self.visitHistory = .init()\n                    try? await saveVisitHistory()\n                    handleError(error, silent: true)\n                }\n            }\n        }\n    }\n    \n    func deactivate() {\n        account.deactivate()\n        api.cleanCaches()\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(api)\n    }\n    \n    static func == (lhs: UserSession, rhs: UserSession) -> Bool {\n        lhs.api == rhs.api\n    }\n    \n    func updateAccount() async throws {\n        if let person, let instance {\n            try await account.update(person: person, software: api.software)\n        }\n    }\n    \n    func saveVisitHistory() async throws {\n        if let visitHistory {\n            try await PersistenceRepository.liveValue.saveVisitHistory(visitHistory, for: account)\n        }\n    }\n\n    func updateInstanceBlock(actorId: ActorIdentifier, shouldBlock: Bool, callback: ((Bool) -> Void)? = nil) {\n        Task {\n            guard !ongoingInstanceBlockRequests.contains(actorId) else {\n                callback?(false)\n                return\n            }\n            \n            ongoingInstanceBlockRequests.insert(actorId)\n            do {\n                let instanceId: Int\n                if let id = self.blocks?.instanceIdOfBlockedInstance(actorId: actorId) {\n                    instanceId = id\n                } else {\n                    instanceId = try await api.getInstanceId(actorId: actorId)\n                }\n                try await api.blockInstance(url: actorId.url, instanceId: instanceId, block: shouldBlock)\n                ongoingInstanceBlockRequests.remove(actorId)\n                callback?(true)\n            } catch {\n                handleError(error)\n                ongoingInstanceBlockRequests.remove(actorId)\n                callback?(false)\n            }\n        }\n    }\n    \n    @MainActor\n    func setVisitHistoryEnabled(_ newValue: Bool) async throws {\n        guard newValue != account.visitHistoryEnabled else { return }\n        account.visitHistoryEnabled = newValue\n        if newValue {\n            visitHistory = .init()\n        } else {\n            visitHistory = nil\n            try await PersistenceRepository.liveValue.saveVisitHistory(.init(), for: account)\n        }\n        AccountsTracker.main.saveAccounts(ofType: .user)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Settings/Options/InternetSpeed.swift",
    "content": "//\n//  InternetSpeed.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-08-02.\n//\n\nimport Foundation\n\nenum InternetSpeed: String, Codable {\n    case debug, slow, fast\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .debug: \"Debug\"\n        case .slow: \"Slow\"\n        case .fast: \"Fast\"\n        }\n    }\n    \n    var id: Self { self }\n    \n    var pageSize: Int {\n        switch self {\n        case .debug: return 11\n        case .slow: return 25\n        case .fast: return 50\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Settings/Options/PostSize.swift",
    "content": "//\n//  PostSize.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-19.\n//\n\nimport Foundation\nimport Icons\nimport QuickSwipes\nimport SwiftUI\n\nenum PostSize: String, CaseIterable, Codable {\n    case compact, tile, headline, large\n    \n    /// Convenience because this check comes up a lot\n    var tiled: Bool { self == .tile }\n    \n    var cornerRadius: CGFloat {\n        switch self {\n        case .tile: Constants.main.largeItemCornerRadius\n        default: Constants.main.standardSpacing\n        }\n    }\n    \n    var quickSwipeIconSize: CGFloat {\n        switch self {\n        case .tile: 18\n        default: 28\n        }\n    }\n    \n    var quickSwipeMinimumDrag: CGFloat {\n        switch self {\n        case .tile: 10\n        default: 20\n        }\n    }\n    \n    var quickSwipeThresholds: QuickSwipeThresholdSet {\n        switch self {\n        case .tile: .init(primary: 40, secondary: 100, tertiary: 160)\n        default: .default\n        }\n    }\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .compact: \"Compact\"\n        case .headline: \"Headline\"\n        case .large: \"Large\"\n        case .tile: \"Tiled\"\n        }\n    }\n    \n    var avatarSize: Int? {\n        switch self {\n        case .compact, .tile:\n            return nil\n        case .headline, .large:\n            return Int(Constants.main.largeAvatarSize * 2)\n        }\n    }\n    \n    var imageSize: CGFloat? {\n        // TODO: Vary this by device?\n        switch self {\n        case .compact, .headline: 128\n        case .tile: 512\n        case .large: nil\n        }\n    }\n    \n    var sectionSpacing: CGFloat {\n        switch self {\n        case .compact: Constants.main.halfSpacing\n        default: Constants.main.standardSpacing\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .compact: .settings.postSizeCompact\n        case .tile: .settings.postSizeTiled\n        case .headline: .settings.postSizeHeadline\n        case .large: .settings.postSizeLarge\n        }\n    }\n    \n    var markReadOffset: Int {\n        switch self {\n        case .compact, .tile: 4\n        case .headline: 2\n        case .large: 1\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Models/Settings/Options/ThumbnailLocation.swift",
    "content": "//\n//  ThumbnailLocation.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-23.\n//\n\nimport Foundation\nimport Icons\n\nenum ThumbnailLocation: String, CaseIterable, Codable {\n    case left, right, none\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .none: \"None\"\n        case .left: \"Left\"\n        case .right: \"Right\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .left: .general.backward\n        case .right: .general.forward\n        case .none: .general.hide\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Protocols/AccountSortMode.swift",
    "content": "//\n//  AccountSortMode.swift\n//  Mlem\n//\n//  Created by Sjmarf on 23/12/2023.\n//\n\nimport SwiftUI\n\nenum AccountSortMode: String, CaseIterable, Codable {\n    case custom, name, instance, mostRecent\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .name:\n            return \"Name\"\n        case .instance:\n            return \"Instance\"\n        case .mostRecent:\n            return \"Most Recent\"\n        case .custom:\n            return \"Custom Order\"\n        }\n    }\n    \n    var systemImage: String {\n        switch self {\n        case .name:\n            return \"textformat\"\n        case .instance:\n            return \"at\"\n        case .mostRecent:\n            return \"clock\"\n        case .custom:\n            return \"line.3.horizontal.decrease\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Protocols/AssociatedColor.swift",
    "content": "//\n//  AssociatedColor.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-10-31.\n//\n\nimport Foundation\nimport SwiftUI\n\nprotocol AssociatedColor {\n    var color: Color? { get }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/ApiClient+Extensions.swift",
    "content": "//\n//  ApiClient+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 02/06/2024.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport OpenGraph\nimport PhotosUI\nimport SwiftUI\n\nextension ApiClient {\n    func isActive(appState: AppState) -> Bool {\n        appState.guestSession.api === self || appState.activeSessions.contains(where: { $0.api === self })\n    }\n    \n    func canInteract(appState: AppState) -> Bool { isActive(appState: appState) && token != nil }\n    \n    func getPostLinkOrUseOpenGraph(url: URL) async throws -> PostLink {\n        if  try await self.supports(.fetchLinkMetadata) {\n            return try await self.getPostLink(url: url)\n        }\n        let metadata = try await OpenGraph.fetch(url: url)\n        let thumbnailUrl = metadata[.image].map { URL(string: $0) } ?? nil\n        return .init(content: url, thumbnail: thumbnailUrl, label: metadata[.title] ?? url.absoluteString)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Array+Extensions.swift",
    "content": "//\n//  Array+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-10.\n//\n\nimport Foundation\n\nextension Array {\n    subscript(safeIndex index: Int) -> Element? {\n        guard index >= 0, index < endIndex else {\n            return nil\n        }\n        \n        return self[index]\n    }\n    \n    mutating func appendIfPresent(_ newElement: Element?) {\n        if let newElement {\n            self.append(newElement)\n        }\n    }\n}\n\nextension Sequence where Element: Hashable {\n    func uniqued() -> [Element] {\n        var set = Set<Element>()\n        return filter { set.insert($0).inserted }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/BackendClient+Extensions.swift",
    "content": "//\n//  MlemBackend+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-23.\n//\n\nimport MlemBackend\n\nextension BackendClient {\n    static var main: BackendClient = .init()\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Binding+Extensions.swift",
    "content": "//\n//  Binding+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 06/07/2024.\n//\n\nimport SwiftUI\n\nextension Binding where Value == Bool {\n    func invert() -> Binding<Bool> {\n        .init(\n            get: { !wrappedValue },\n            set: { self.wrappedValue = !$0 }\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Blockable+Extensions.swift",
    "content": "//\n//  Blockable+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-02-10.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension Blockable {\n    func blocked(environment: EnvironmentValues) -> Bool {\n        if self is any InstanceActionProviding,\n           let session = (environment.appState.firstSession as? UserSession) {\n            return session.blocks?.contains(instanceActorId: actorId) ?? self.blocked.realizedValue\n        }\n        return self.blocked.realizedValue\n    }\n    \n    var toggleBlocked: ((Set<FeedbackType>, ((Bool) -> Void)?) -> Void)? {\n        if let updateBlocked = self.updateBlocked {\n            return { toggleBlocked(updateBlocked: updateBlocked, feedback: $0, callback: $1) }\n        }\n        return nil\n    }\n    \n    private func toggleBlocked(\n        updateBlocked: @escaping (Bool, ((Bool) -> Void)?) -> Void,\n        feedback: Set<FeedbackType>,\n        callback: ((Bool) -> Void)? = nil) {\n            if feedback.contains(.toast) {\n                if !blocked.realizedValue {\n                    ToastModel.main.add(\n                        .undoable(\n                            \"Blocked\",\n                            icon: .lemmy.block,\n                            callback: {\n                                updateBlocked(false, callback)\n                            },\n                            color: .themedNegative\n                        )\n                    )\n                } else {\n                    ToastModel.main.add(\n                        .undoable(\n                            \"Unblocked\",\n                            icon: .lemmy.unblock,\n                            callback: {\n                                updateBlocked(true, callback)\n                            },\n                            color: .themedPrimary\n                        )\n                    )\n                }\n            }\n            updateBlocked(!blocked.realizedValue, callback)\n        }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Bundle+Extensions.swift",
    "content": "//\n//  Bundle+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2024.\n//\n\nimport Foundation\n\nextension Bundle {\n    var releaseVersionNumber: String? {\n        infoDictionary?[\"CFBundleShortVersionString\"] as? String\n    }\n\n    var buildVersionNumber: String? {\n        infoDictionary?[\"CFBundleVersion\"] as? String\n    }\n    \n    var isTestFlight: Bool {\n        // https://stackoverflow.com/a/26113597/17629371\n        appStoreReceiptURL?.lastPathComponent == \"sandboxReceipt\"\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/CGFloat+Extensions.swift",
    "content": "//\n//  CGFloat+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-17.\n//\n\nimport Foundation\n\nextension CGFloat {\n    func bounded(lower: CGFloat, upper: CGFloat) -> CGFloat {\n        if self < lower {\n            return lower\n        }\n        if self > upper {\n            return upper\n        }\n        return self\n    }\n    \n    func stepped(by increment: CGFloat) -> CGFloat {\n        (self / increment).rounded() * increment\n    }\n    \n    /// Returns the value of this CGFloat bounded within the given range. If this float is above softMax, the returned\n    /// value will asymptotically approach hardMax, and likewise for softMin and hardMin\n    func softBounded(softMin: CGFloat, hardMin: CGFloat, softMax: CGFloat, hardMax: CGFloat) -> CGFloat {\n        guard softMin > hardMin, softMax < hardMax, softMin < softMax else {\n            if softMin <= hardMin {\n                assertionFailure(\"Soft min \\(softMin) <= hard min \\(hardMin)\")\n            }\n            if softMax >= hardMax {\n                assertionFailure(\"Soft max \\(softMax) >= hard max \\(hardMax)\")\n            }\n            if softMin >= softMax {\n                assertionFailure(\"Soft min \\(softMin) >= soft max \\(softMax)\")\n            }\n            return self\n        }\n        \n        if self > softMax {\n            let headroom = hardMax - softMax\n            let excess = self - softMax\n            let scaledExcess = headroom - asymptote(x: excess, n: headroom)\n            return softMax + scaledExcess\n        }\n        \n        if self < softMin {\n            let headroom = softMin - hardMin\n            let excess = softMin - self\n            let scaledExcess = asymptote(x: excess, n: headroom) - headroom\n            return softMin + scaledExcess\n        }\n        \n        return self\n    }\n    \n    /// Base asymptotic function used for softBounded, where x is the value to scale and n is the asymptotic bound\n    private func asymptote(x: CGFloat, n: CGFloat) -> CGFloat { // swiftlint:disable:this identifier_name\n        n / (((1 / n) * x) + 1)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/CGPoint+Extensions.swift",
    "content": "//\n//  CGPoint+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-31.\n//\n\nimport Foundation\n\nextension CGPoint {\n    static func + (lhs: Self, rhs: Self) -> Self {\n        .init(x: lhs.x + rhs.x, y: lhs.y + rhs.y)\n    }\n    \n    static func += (lhs: inout Self, rhs: Self) {\n        lhs = lhs + rhs\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/CGSize+Extensions.swift",
    "content": "//\n//  CGSize+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-02-24.\n//\n\nimport Foundation\n\nextension CGSize {\n    var aspectRatio: Double {\n        height / width\n    }\n    \n    static func + (lhs: Self, rhs: Self) -> Self {\n        .init(width: lhs.width + rhs.width, height: lhs.height + rhs.height)\n    }\n    \n    static func - (lhs: Self, rhs: Self) -> Self {\n        .init(width: lhs.width - rhs.width, height: lhs.height - rhs.height)\n    }\n    \n    static func += (lhs: inout Self, rhs: Self) {\n        lhs = lhs + rhs\n    }\n    \n    func scaled(by factor: CGFloat) -> CGSize {\n        .init(width: width * factor, height: height * factor)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Calendar+Extensions.swift",
    "content": "//\n//  Calendar+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-29.\n//\n\nimport Foundation\n\nextension Calendar {\n    func daysSince(_ from: Date) -> Int? {\n        let fromDate = startOfDay(for: from)\n        let toDate = startOfDay(for: Date.now)\n        let numberOfDays = dateComponents([.day], from: fromDate, to: toDate)\n        \n        return numberOfDays.day\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/CaptchaDifficulty+Extensions.swift",
    "content": "//\n//  CaptchaDifficulty+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 29/07/2024.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nextension CaptchaDifficulty {\n    var label: LocalizedStringResource {\n        switch self {\n        case .easy: \"Easy\"\n        case .medium: \"Medium\"\n        case .hard: \"Hard\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Color+Extensions.swift",
    "content": "//\n//  Color+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-29.\n//\n\nimport Foundation\nimport SwiftUI\n\nextension Color {\n    init(light: UIColor, dark: UIColor) {\n        self.init(uiColor: UIColor { traits in\n            traits.userInterfaceStyle == .dark ? dark : light\n        })\n    }\n    \n    init(light: Color, dark: Color) {\n        self.init(uiColor: UIColor { traits in\n            traits.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light)\n        })\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/CommentSortType+Extensions.swift",
    "content": "//\n//  CommentSortType+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-17.\n//\n\nimport Foundation\nimport Icons\nimport MlemMiddleware\n\nextension CommentSortType {\n    func label(timeRangeFormat: SortTimeRange.FormatStyle = .timescaleFull) -> String {\n        switch self {\n        case .new:\n            .init(localized: \"New\")\n        case .old:\n            .init(localized: \"Old\")\n        case .hot:\n            .init(localized: \"Hot\")\n        case .controversial:\n            .init(localized: \"Controversial\")\n        case let .top(timeRange):\n            timeRange.label(name: \"Top\", prefix: \"Top:\", format: timeRangeFormat)\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .new: .lemmy.newSort\n        case .old: .lemmy.oldSort\n        case .hot: .lemmy.hotSort\n        case .controversial: .lemmy.controversialSort\n        case .top: .lemmy.topSort\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/ActorIdentifiable+Extensions.swift",
    "content": "//\n//  ActorIdentifiable+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 02/07/2024.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nextension ActorIdentifiable {\n    func openInstanceAction(navigation: NavigationLayer?) -> BasicAction {\n        let callback: (@MainActor () -> Void)?\n        if let navigation {\n            callback = { navigation.push(.hostInstance(of: self)) }\n        } else {\n            callback = nil\n        }\n        return .init(\n            id: \"instance\\(actorId)\",\n            appearance: .init(label: host, color: .themedNeutralAccent, icon: Icons.instance),\n            callback: callback\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Captcha+Extensions.swift",
    "content": "//\n//  Captcha+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 06/09/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension Captcha {\n    var uiImage: UIImage? {\n        .init(data: imageData)\n    }\n    \n    var image: Image? {\n        if let uiImage {\n            .init(uiImage: uiImage)\n        } else {\n            nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Comment/Comment+Actions.swift",
    "content": "//\n//  Comment+Actions.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-19.\n//\n\nimport Haptics\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nextension Comment {\n    // MARK: - Readouts\n    \n    func readout(type: CommentBarConfiguration.ReadoutType, showColor: Bool) -> Readout? {\n        switch type {\n        case .created: createdReadout\n        // swiftlint:disable:next void_function_in_ternary\n        case .score: downvotesEnabled ? scoreReadout(showColor: showColor) : upvoteReadout(showColor: showColor)\n        case .upvote: upvoteReadout(showColor: showColor)\n        case .downvote: downvotesEnabled ? downvoteReadout(showColor: showColor) : nil\n        case .comment: commentReadout\n        case .saved: savedReadout(showColor: showColor)\n        }\n    }\n\n    func readout(type: ReplyBarConfiguration.ReadoutType, showColor: Bool) -> Readout? {\n        switch type {\n        case .created: createdReadout\n        // swiftlint:disable:next void_function_in_ternary\n        case .score: downvotesEnabled ? scoreReadout(showColor: showColor) : upvoteReadout(showColor: showColor)\n        case .upvote: upvoteReadout(showColor: showColor)\n        case .downvote: downvotesEnabled ? downvoteReadout(showColor: showColor) : nil\n        case .comment: commentReadout\n        case .saved: savedReadout(showColor: showColor)\n        }\n    }\n    \n    // MARK: - Counters\n    \n    func counter(\n        appState: AppState,\n        type: CommentBarConfiguration.CounterType,\n        commentTreeTracker: CommentTreeTracker? = nil\n    ) -> Counter? {\n        switch type {\n        case .score: scoreCounter(appState: appState, downvotesEnabled: downvotesEnabled)\n        case .upvote: upvoteCounter(appState: appState)\n        case .downvote: downvotesEnabled ? downvoteCounter(appState: appState, downvotesEnabled: downvotesEnabled) : nil\n        case .reply: replyCounter(appState: appState, commentTreeTracker: commentTreeTracker)\n        }\n    }\n\n    func counter(\n        appState: AppState,\n        type: ReplyBarConfiguration.CounterType,\n        commentTreeTracker: CommentTreeTracker? = nil\n    ) -> Counter? {\n        switch type {\n        case .score: scoreCounter(appState: appState, downvotesEnabled: downvotesEnabled)\n        case .upvote: upvoteCounter(appState: appState)\n        case .downvote: downvotesEnabled ? downvoteCounter(appState: appState, downvotesEnabled: downvotesEnabled) : nil\n        case .reply: replyCounter(appState: appState, commentTreeTracker: commentTreeTracker)\n        }\n    }\n    \n    // MARK: - Actions\n    \n    func createImageAction(navigation: NavigationLayer, commentTreeTracker: CommentTreeTracker?) -> BasicAction {\n        .init(\n            id: \"exportAsImage\\(uid)\",\n            appearance: .createImage()) {\n                navigation.openSheet(.exportCommentImage(self, tracker: commentTreeTracker))\n            }\n    }\n    \n    func editAction(appState: AppState) -> BasicAction {\n        .init(\n            id: \"edit\\(uid)\",\n            appearance: .edit(),\n            callback: api.canInteract(appState: appState)\n            ? { @MainActor in NavigationModel.main.openSheet(.editComment(self, context: nil)) }\n            : nil\n        )\n    }\n    \n    func viewVotesAction() -> BasicAction {\n        let callback: (@MainActor () -> Void)? = canModerate && api.supports(.viewVotes, defaultValue: true)\n        ? { @MainActor in NavigationModel.main.openSheet(.votesList(.comment(self))) }\n        : nil\n        return .init(\n            id: \"viewVotes\\(uid)\",\n            appearance: .viewVotes(),\n            callback: callback\n        )\n    }\n    \n    func markReadAction(appState: AppState, notification: InboxNotification, feedback: Set<FeedbackType> = []) -> BasicAction {\n        .init(\n            id: \"markRead\\(uid)\",\n            appearance: .markRead(isOn: notification.read),\n            callback: api.canInteract(appState: appState) ? {\n                @MainActor in\n                notification.toggleRead()\n                HapticManager.main.play(haptic: .lightSuccess, tier: .low)\n            } : nil\n        )\n    }\n    \n    func collapseAction(commentTreeTracker: CommentTreeTracker? = nil) -> BasicAction {\n        .init(\n            id: \"collapse\\(uid)\",\n            appearance: .collapse(),\n            callback: { @MainActor in\n                withAnimation(UIAccessibility.isReduceMotionEnabled ? nil : .default) {\n                    commentTreeTracker?.nodesKeyedByActorId[self.actorId]?.collapsed.toggle()\n                }\n            }\n        )\n    }\n    \n    func collapseParentAction(commentTreeTracker: CommentTreeTracker? = nil) -> BasicAction {\n        .init(\n            id: \"collapseParent\\(uid)\",\n            appearance: .collapseParent(),\n            callback: { @MainActor in\n                withAnimation(UIAccessibility.isReduceMotionEnabled ? nil : .default) {\n                    guard let comment = commentTreeTracker?.nodesKeyedByActorId[self.actorId] else { return }\n                    (comment.parent ?? comment).collapsed.toggle()\n                }\n            }\n        )\n    }\n    \n    func collapseToTopAction(commentTreeTracker: CommentTreeTracker? = nil) -> BasicAction {\n        .init(\n            id: \"collapseToTop\\(uid)\",\n            appearance: .collapseToTop(),\n            callback: { @MainActor in\n                withAnimation(UIAccessibility.isReduceMotionEnabled ? nil : .default) {\n                    commentTreeTracker?.nodesKeyedByActorId[self.actorId]?.topParent.collapsed.toggle()\n                }\n            }\n        )\n    }\n    \n    // MARK: - Action Groups\n    \n    // swiftlint:disable:next cyclomatic_complexity\n    func action(\n        appState: AppState,\n        type: CommentBarConfiguration.ActionType,\n        navigation: NavigationLayer?,\n        commentTreeTracker: CommentTreeTracker? = nil,\n        communityContext: Community? = nil,\n        reportContext: Report? = nil\n    ) -> (any Action)? {\n        switch type {\n        case .upvote: if let upvoteAction = upvoteAction(appState: appState, feedback: [.haptic]) { return upvoteAction }\n        case .downvote: if let downvoteAction = downvoteAction(appState: appState, feedback: [.haptic]) { return downvoteAction }\n        case .save: return saveAction(appState: appState, feedback: [.haptic])\n        case .reply: return replyAction(appState: appState, commentTreeTracker: commentTreeTracker)\n        case .share: return shareAction(navigation: navigation)\n        case .selectText: return selectTextAction()\n        case .report: return reportAction(appState: appState, communityContext: communityContext)\n        case .resolve: return reportContext?.resolveAction(appState: appState, feedback: [.haptic])\n        case .remove: return removeAction(appState: appState)\n        case .ban: return reportContext?.contextualBanAction(appState: appState)\n        case .collapse: return collapseAction(commentTreeTracker: commentTreeTracker)\n        case .collapseParent: return collapseParentAction(commentTreeTracker: commentTreeTracker)\n        case .collapseToTop: return collapseToTopAction(commentTreeTracker: commentTreeTracker)\n        }\n        return nil\n    }\n    \n    func action(\n        appState: AppState,\n        type: ReplyBarConfiguration.ActionType,\n        navigation: NavigationLayer?,\n        notification: InboxNotification,\n        commentTreeTracker: CommentTreeTracker? = nil,\n        communityContext: Community? = nil,\n        reportContext: Report? = nil\n    ) -> (any Action)? {\n        switch type {\n        case .upvote: if let upvoteAction = upvoteAction(appState: appState, feedback: [.haptic]) { return upvoteAction }\n        case .downvote: if let downvoteAction = downvoteAction(appState: appState, feedback: [.haptic]) { return downvoteAction }\n        case .save: return saveAction(appState: appState, feedback: [.haptic])\n        case .reply: return replyAction(appState: appState, commentTreeTracker: commentTreeTracker)\n        case .selectText: return selectTextAction()\n        case .report: return reportAction(appState: appState, communityContext: communityContext)\n        case .markRead: return markReadAction(appState: appState, notification: notification)\n        }\n        return nil\n    }\n    \n    // MARK: - Action Groups\n    \n    @ActionBuilder\n    func allMenuActions(\n        appState: AppState,\n        expanded: Bool = false,\n        feedback: Set<FeedbackType> = [.haptic, .toast],\n        showAllActions: Bool = true,\n        navigation: NavigationLayer?,\n        notification: InboxNotification? = nil,\n        commentTreeTracker: CommentTreeTracker? = nil,\n        report: Report? = nil\n    ) -> [any Action] {\n        basicMenuActions(\n            appState: appState,\n            feedback: feedback,\n            navigation: navigation,\n            notification: notification,\n            commentTreeTracker: commentTreeTracker\n        )\n        if canModerate {\n            ActionGroup(\n                appearance: .init(label: \"Moderation...\", color: .themedModeration, icon: Icons.moderation),\n                displayMode: Settings.get(\\.menus_modActionGrouping) == .divider || expanded ? .section : .disclosure\n            ) {\n                moderatorMenuActions(appState: appState, feedback: feedback, showAllActions: showAllActions, report: report)\n            }\n        }\n    }\n    \n    @ActionBuilder\n    func basicMenuActions(\n        appState: AppState,\n        feedback: Set<FeedbackType> = [.haptic, .toast],\n        navigation: NavigationLayer?,\n        notification: InboxNotification? = nil,\n        commentTreeTracker: CommentTreeTracker? = nil\n    ) -> [any Action] {\n        ActionGroup(displayMode: .compactSection) {\n            if let upvoteAction = upvoteAction(appState: appState, feedback: feedback) { upvoteAction }\n            if let downvoteAction = downvoteAction(\n                appState: appState,\n                feedback: feedback) { downvoteAction }\n            if let saveAction = saveAction(appState: appState, feedback: feedback) { saveAction }\n            replyAction(appState: appState, commentTreeTracker: commentTreeTracker)\n            if let notification {\n                markReadAction(appState: appState, notification: notification, feedback: feedback)\n            }\n            if !deleted {\n                selectTextAction()\n            }\n            shareAction(navigation: navigation)\n            \n            if let navigation, notification == nil {\n                createImageAction(navigation: navigation, commentTreeTracker: commentTreeTracker)\n            }\n            \n            if isOwnComment {\n                editAction(appState: appState)\n                deleteAction(appState: appState, feedback: feedback)\n            } else {\n                if !canModerate, !deleted {\n                    reportAction(appState: appState)\n                }\n                if let blockCreatorAction = blockCreatorAction(appState: appState, feedback: feedback) { blockCreatorAction }\n            }\n        }\n    }\n    \n    @ActionBuilder\n    func moderatorMenuActions(\n        appState: AppState,\n        feedback: Set<FeedbackType> = [.haptic, .toast],\n        showAllActions: Bool = true,\n        report: Report? = nil\n    ) -> [any Action] {\n        let viewVotesIsPossible = api.supports(.viewVotes, defaultValue: false)\n        \n        if viewVotesIsPossible, showAllActions || Settings.get(\\.menus_allModActions) {\n            viewVotesAction()\n        }\n        if !isOwnComment {\n            removeAction(appState: appState).disabled(!canModerate)\n            if let creator = creator.value, let community = community.value {\n                creator.banActions(appState: appState, community: community, withUserLabel: true)\n            }\n        }\n        if api.isAdmin, api.supports(.purgeContent, defaultValue: false) {\n            purgeAction(appState: appState)\n            if !isOwnComment,\n            let purgeCreatorAction = purgeCreatorAction(appState: appState) {\n                purgeCreatorAction\n            }\n        }\n        if let report {\n            ActionGroup {\n                report.menuActions(appState: appState)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Comment/Comment+Extensions.swift",
    "content": "//\n//  Comment+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-01-21.\n//\n\nimport MlemMiddleware\n\nextension Comment {\n    func shouldShowLoadingSymbol(for barConfiguration: CommentBarConfiguration? = nil) -> Bool {\n        // TODO: NOW really?\n        false\n    }\n    \n    var shouldHideInFeed: Bool {\n        (creator.value_?.shouldHideInFeed ?? false) || purged\n    }\n    \n    var isOwnComment: Bool { creatorId == api.myPerson?.id }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Community/Community+Extensions.swift",
    "content": "//\n//  Community+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-02-20.\n//\n\nimport MlemMiddleware\n\nextension Community {\n    var shouldHideInFeed: Bool { blocked_.realizedValue }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/CommunityOrPersonStub+Extensions.swift",
    "content": "//\n//  CommunityOrPersonStub+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/05/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nextension CommunityOrPerson {\n    func copyFullNameWithPrefix(feedback: Set<FeedbackType> = [.toast]) {\n        if feedback.contains(.toast) {\n            ToastModel.main.add(.success(\"Copied\"))\n        }\n        UIPasteboard.general.string = fullNameWithPrefix\n    }\n    \n    func copyNameAction(feedback: Set<FeedbackType> = [.toast]) -> BasicAction {\n        .init(\n            id: \"copyName\\(actorId)\",\n            appearance: .init(\n                label: \"Copy Name\",\n                color: .themedNeutralAccent,\n                icon: Icons.copy,\n                swipeIcon2: Icons.copyFill\n            ),\n            callback: { self.copyFullNameWithPrefix(feedback: feedback) }\n        )\n    }\n    \n    func attributedName(\n        showInstance: Bool = true,\n        font: Font = .body,\n        palette: Theming.Palette,\n        nameColor: ThemedColor = .themedSecondary,\n        instanceColor: ThemedColor = .themedTertiary\n    ) -> AttributedString? {\n        var outputString = AttributedString(name)\n        outputString.foregroundColor = nameColor.resolve(with: palette)\n        outputString.font = font.bold()\n        \n        if showInstance {\n            var instanceString = AttributedString(\"@\\(host)\")\n            instanceString.foregroundColor = instanceColor.resolve(with: palette)\n            instanceString.font = font\n            outputString += instanceString\n        }\n        \n        outputString.link = actorId.url\n        return outputString\n    }\n    \n    func nameTextView(\n        showFlairs: Bool,\n        showInstance: Bool = true,\n        communityContext: Community? = nil,\n        font: Font = .body,\n        palette: Theming.Palette,\n        nameColor: ThemedColor = .themedSecondary,\n        instanceColor: ThemedColor = .themedTertiary\n    ) -> Text {\n        let attributedName = attributedName(\n            showInstance: showInstance,\n            font: font,\n            palette: palette,\n            nameColor: nameColor,\n            instanceColor: instanceColor\n        )\n        if showFlairs, let flairs = (self as? Person)?.flairs(communityContext: communityContext) {\n            return flairs.textView.font(font) + Text(attributedName ?? \"\")\n        } else {\n            return Text(attributedName ?? \"\")\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/DeletableProviding+Extensions.swift",
    "content": "//\n//  DeletableProviding+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 22/07/2024.\n//\n\nimport MlemMiddleware\n\nextension DeletableProviding {\n    func toggleDeleted(feedback: Set<FeedbackType>) {\n        if feedback.contains(.toast), !deleted {\n            toggleDeleted { status in\n                switch status {\n                case .success:\n                    if self is any Message1Providing, !self.api.supports(.undeletePrivateMessages, defaultValue: true) {\n                        ToastModel.main.add(\n                            .basic(\n                                \"Deleted\",\n                                icon: .general.delete,\n                                color: .themedNegative\n                            )\n                        )\n                    } else {\n                        ToastModel.main.add(\n                            .undoable(\n                                \"Deleted\",\n                                icon: .general.delete,\n                                callback: { self.updateDeleted(false, callback: nil) },\n                                color: .themedNegative\n                            )\n                        )\n                    }\n                case .failure:\n                    ToastModel.main.add(.failure(\"Failed to delete post!\"))\n                }\n            }\n        } else {\n            toggleDeleted()\n        }\n    }\n    \n    func deleteAction(appState: AppState, feedback: Set<FeedbackType>) -> BasicAction {\n        .init(\n            id: \"delete\\(uid)\",\n            appearance: .init(\n                label: deleted ? \"Restore\" : \"Delete\",\n                isOn: deleted,\n                isDestructive: !deleted,\n                color: deleted ? .themedPositive : .themedNegative,\n                icon: deleted ? Icons.undelete : Icons.delete\n            ),\n            confirmationPrompt: deleted ? nil : \"Really delete?\",\n            callback: api.canInteract(appState: appState) ? { @MainActor in self.toggleDeleted(feedback: feedback) } : nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/InboxItemProviding+Extensions.swift",
    "content": "//\n//  InboxItemProviding+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport Haptics\nimport MlemMiddleware\n\nextension InboxItemProviding {    \n    func toggleRead(feedback: Set<FeedbackType>) {\n        if feedback.contains(.haptic) {\n            HapticManager.main.play(haptic: .lightSuccess, tier: .low)\n        }\n        toggleRead()\n    }\n    \n    func markReadAction(appState: AppState, feedback: Set<FeedbackType> = []) -> BasicAction {\n        .init(\n            id: \"markRead\\(uid)\",\n            appearance: .markRead(isOn: read),\n            callback: api.canInteract(appState: appState) ? { @MainActor in self.toggleRead(feedback: feedback) } : nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/InboxItemType+Extensions.swift",
    "content": "//\n//  InboxItemType+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-17.\n//\n\nimport Foundation\nimport Icons\nimport MlemMiddleware\n\nextension InboxItemType {\n    var label: LocalizedStringResource {\n        switch self {\n        case .reply: \"Replies\"\n        case .mention: \"Mentions\"\n        case .message: \"Messages\"\n        case .postReport: \"Post Reports\"\n        case .commentReport: \"Comment Reports\"\n        case .messageReport: \"Message Reports\"\n        case .registrationApplication: \"Registration Applications\"\n        }\n    }\n    \n    fileprivate var labelOnly: LocalizedStringResource {\n        switch self {\n        case .reply: \"Replies Only\"\n        case .mention: \"Mentions Only\"\n        case .message: \"Messages Only\"\n        case .postReport: \"Post Reports Only\"\n        case .commentReport: \"Comment Reports Only\"\n        case .messageReport: \"Message Reports Only\"\n        case .registrationApplication: \"Applications Only\"\n        }\n    }\n    \n    fileprivate var labelExcept: LocalizedStringResource {\n        switch self {\n        case .reply: \"Except Replies\"\n        case .mention: \"Except Mentions\"\n        case .message: \"Except Messages\"\n        case .postReport: \"Except Post Reports\"\n        case .commentReport: \"Except Comment Reports\"\n        case .messageReport: \"Except Message Reports\"\n        case .registrationApplication: \"Except Applications\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .reply: .lemmy.reply\n        case .mention: .lemmy.mention\n        case .message: .lemmy.message\n        case .postReport: .lemmy.post\n        case .commentReport: .lemmy.replies\n        case .messageReport: .lemmy.report\n        case .registrationApplication: .lemmy.registrationApplication\n        }\n    }\n    \n    var requiredAccountType: AccountType {\n        switch self {\n        case .reply: .user\n        case .mention: .user\n        case .message: .user\n        case .postReport: .moderator\n        case .commentReport: .moderator\n        case .messageReport: .admin\n        case .registrationApplication: .admin\n        }\n    }\n}\n\nextension Sequence<InboxItemType> {\n    func label(accountType: AccountType) -> String {\n        let items = Set(filter { accountType >= $0.requiredAccountType })\n        let allItems = Set<InboxItemType>.all.filter { accountType >= $0.requiredAccountType }\n        \n        if items.isEmpty { return .init(localized: \"None\") }\n        \n        if items == allItems {\n            return .init(localized: \"All\")\n        }\n        if items == .personal {\n            return .init(localized: \"Personal Only\")\n        }\n        if accountType >= .moderator, items == .reports.filter({ accountType >= $0.requiredAccountType }) {\n            return .init(localized: \"Reports Only\")\n        }\n        \n        if accountType == .admin, items == .moderatorAndAdmin {\n            return .init(localized: \"Mod Mail Only\")\n        }\n        \n        if items.count == 2 {\n            return items.map { String(localized: $0.label) }.sorted().joined(separator: \" & \")\n        }\n        if items.count == 1, let first = items.first {\n            return .init(localized: first.labelOnly)\n        }\n        \n        let disabledItems = allItems.subtracting(items)\n        if disabledItems.count == 1, let first = disabledItems.first {\n            return .init(localized: first.labelExcept)\n        }\n        \n        return .init(localized: \"Some\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Instance+Extensions.swift",
    "content": "//\n//  Instance3+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-02.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport MlemBackend\n\nprivate let uptimeSupportedInstances: Set<String> = [\n    \"aussie.zone\",\n    \"beehaw.org\",\n    \"discuss.online\",\n    \"discuss.tchncs.de\",\n    \"dubvee.org\",\n    \"feddit.org\",\n    \"feddit.dk\",\n    \"hexbear.net\",\n    \"infosec.pub\",\n    \"jlai.lu\",\n    \"lemdro.id\",\n    \"lemm.ee\",\n    \"lemmings.world\",\n    \"lemmy.blahaj.zone\",\n    \"lemmy.ca\",\n    \"lemmy.dbzer0.com\",\n    \"lemmy.eco.br\",\n    \"lemmy.ml\",\n    \"lemmy.myserv.one\",\n    \"lemmy.nz\",\n    \"lemmy.world\",\n    \"lemmy.zip\",\n    \"literature.cafe\",\n    \"mander.xyz\",\n    \"midwest.social\",\n    \"programming.dev\",\n    \"sh.itjust.works\",\n    \"slrpnk.net\",\n    \"sopuli.xyz\",\n    \"startrek.website\",\n    \"szmer.info\",\n    \"toast.ooo\"\n]\n\nextension Instance {\n    func slurRegex() -> Regex<AnyRegexOutput>? {\n        do {\n            if let regex = slurFilterRegex.value as? String {\n                return try .init(regex)\n            }\n        } catch {\n            handleError(error, silent: true)\n        }\n        return nil\n    }\n    \n    var instanceSummary: InstanceSummary? {\n        if let userCount = userCount.value,\n           let software = software.value {\n            return .init(\n                displayName: displayName,\n                name: name,\n                totalUsers: userCount,\n                avatar: avatar,\n                software: .init(from: software)\n            )\n        }\n        return nil\n    }\n    \n    var canFetchUptime: Bool { uptimeSupportedInstances.contains(host) }\n    \n    var uptimeDataUrl: URL? {\n        guard canFetchUptime else { return nil }\n        let name = \"_\\(host.replacingOccurrences(of: \".\", with: \"-\"))\"\n        return URL(string: \"https://lemmy-status.org/api/v1/endpoints/\\(name)/statuses?page=1\")\n    }\n    \n    var uptimeFrontendUrl: URL? {\n        guard canFetchUptime else { return nil }\n        let name = \"_\\(host.replacingOccurrences(of: \".\", with: \"-\"))\"\n        return URL(string: \"https://lemmy-status.org/endpoints/\\(name)\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Instance3+Extensions.swift",
    "content": ""
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Interactable/InteractableProviding+Actions.swift",
    "content": "//\n//  InteractableProviding+Actions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-01-24.\n//\n\nimport MlemMiddleware\nimport Theming\nimport os\n\n// Methods to support actions\n\nextension InteractableProviding {\n        \n    // MARK: Actions\n    \n    func upvoteAction(appState: AppState, feedback: Set<FeedbackType> = []) -> BasicAction? {\n        guard let toggleUpvoted, let votes = votes.value else { return nil }\n        return .init(id: \"upvote\\(uid)\",\n                     appearance: .upvote(isOn: votes.myVote == .upvote),\n                     callback: api.canInteract(appState: appState) ? { @MainActor in toggleUpvoted(feedback) } : nil\n        )\n    }\n    \n    func downvoteAction(appState: AppState, feedback: Set<FeedbackType> = []) -> BasicAction? {\n        guard let toggleDownvoted, let votes = votes.value else { return nil }\n        return .init(\n            id: \"downvote\\(uid)\",\n            appearance: .downvote(isOn: votes.myVote == .downvote),\n            callback: api.canInteract(appState: appState) && downvotesEnabled\n            ? { @MainActor in toggleDownvoted(feedback) }\n            : nil\n        )\n    }\n    \n    func saveAction(appState: AppState, feedback: Set<FeedbackType> = []) -> BasicAction? {\n        guard let toggleSaved, let saved = saved.value else { return nil }\n        return .init(\n            id: \"save\\(uid)\",\n            appearance: .save(isOn: saved),\n            callback: api.canInteract(appState: appState)\n            ? { @MainActor in toggleSaved(feedback) }\n            : nil\n        )\n    }\n    \n    func replyAction(appState: AppState, commentTreeTracker: CommentTreeTracker? = nil) -> BasicAction {\n        return .init(\n            id: \"reply\\(uid)\",\n            appearance: .reply(),\n            callback: api.canInteract(appState: appState)\n            ? { @MainActor in self.showReplySheet(commentTreeTracker: commentTreeTracker) }\n            : nil\n        )\n    }\n    \n    func blockCreatorAction(appState: AppState, feedback: Set<FeedbackType> = [], showConfirmation: Bool = true) -> BasicAction? {\n        guard let creator = creator.value,\n              let toggleBlocked = creator.toggleBlocked else { return nil }\n        return .init(\n            id: \"blockCreator\\(uid)\",\n            appearance: .blockCreator(),\n            confirmationPrompt: showConfirmation ? \"Really block this user?\" : nil,\n            callback: api.canInteract(appState: appState)\n            ? { @MainActor in toggleBlocked(feedback, nil) }\n            : nil\n        )\n    }\n    \n    func purgeCreatorAction(appState: AppState) -> BasicAction? {\n        guard let creator = creator.value else { return nil }\n        return .init(\n            id: \"purgeCreator\\(uid)\",\n            appearance: .purgePerson(),\n            callback: api.canInteract(appState: appState) && api.isAdmin\n            ? { @MainActor in creator.showPurgeSheet() }\n            : nil\n        )\n    }\n    \n    // MARK: Readouts\n    \n    var createdReadout: Readout {\n        .init(\n            id: \"created\\(uid)\",\n            label: (updated ?? created).getShortRelativeTime(),\n            icon: updated == nil ? Icons.time : Icons.updated\n        )\n    }\n    \n    func scoreReadout(showColor: Bool) -> Readout? {\n        guard let votes = votes.value else { return nil }\n        let icon: String\n        let color: ThemedColor?\n        switch votes.myVote {\n        case .upvote:\n            icon = Icons.upvoteSquareFill\n            color = .themedUpvote\n        case .downvote:\n            icon = Icons.downvoteSquareFill\n            color = .themedDownvote\n        default:\n            icon = Icons.upvoteSquare\n            color = nil\n        }\n        return Readout(\n            id: \"score\\(uid)\",\n            label: votes.total.description,\n            icon: icon,\n            color: showColor ? color : nil\n        )\n    }\n    \n    func upvoteReadout(showColor: Bool) -> Readout? {\n        guard let votes = votes.value else { return nil }\n        let isOn = votes.myVote == .upvote\n        return Readout(\n            id: \"upvote\\(uid)\",\n            label: votes.upvotes.description,\n            icon: isOn ? Icons.upvoteSquareFill : Icons.upvoteSquare,\n            color: isOn && showColor ? .themedUpvote : nil\n        )\n    }\n    \n    func downvoteReadout(showColor: Bool) -> Readout? {\n        guard let votes = votes.value else { return nil }\n        let isOn = votes.myVote == .downvote\n        return Readout(\n            id: \"downvote\\(uid)\",\n            label: votes.downvotes.description,\n            icon: isOn ? Icons.downvoteSquareFill : Icons.downvoteSquare,\n            color: isOn && showColor ? .themedDownvote : nil\n        )\n    }\n    \n    var commentReadout: Readout? {\n        guard let commentCount = commentCount.value else { return nil }\n        \n        let value: String?\n        if let unreadCount = (self as? Post)?.unreadCommentCount.value,\n           unreadCount > 0,\n           unreadCount != commentCount {\n            value = \"+\\(unreadCount)\"\n        } else {\n            value = nil\n        }\n        \n        return .init(\n            id: \"comment\\(uid)\",\n            label: commentCount.description,\n            icon: Icons.replies,\n            value: value,\n            valueColor: .themedPositive\n        )\n    }\n    \n    func savedReadout(showColor: Bool) -> Readout? {\n        guard let saved = saved.value else { return nil }\n        let isOn = saved\n        return .init(\n            id: \"saved\\(uid)\",\n            label: nil,\n            icon: isOn ? Icons.saveFill : Icons.save,\n            color: isOn && showColor ? .themedSave : nil\n        )\n    }\n    \n    // MARK: Counters\n    \n    func upvoteCounter(appState: AppState) -> Counter? {\n        guard let votes = votes.value,\n              let upvoteAction = upvoteAction(appState: appState, feedback: [.haptic]) else { return nil }\n        return .init(\n            value: votes.upvotes,\n            leadingAction: upvoteAction,\n            trailingAction: nil\n        )\n    }\n    \n    func downvoteCounter(appState: AppState, downvotesEnabled: Bool) -> Counter? {\n        guard let votes = votes.value,\n              let downvoteAction = downvoteAction(\n                appState: appState,\n                feedback: [.haptic]) else { return nil }\n        return .init(\n            value: votes.downvotes,\n            leadingAction: downvoteAction,\n            trailingAction: nil\n        )\n    }\n    \n    func scoreCounter(\n        appState: AppState,\n        downvotesEnabled: Bool\n    ) -> Counter? {\n        guard let votes = votes.value,\n              let upvoteAction = upvoteAction(appState: appState, feedback: [.haptic]) else { return nil }\n        return .init(\n            value: votes.total,\n            leadingAction: upvoteAction,\n            trailingAction: downvoteAction(\n                appState: appState,\n                feedback: [.haptic]\n            )\n        )\n    }\n    \n    func replyCounter(appState: AppState, commentTreeTracker: CommentTreeTracker? = nil) -> Counter? {\n        guard let commentCount = self.commentCount.value else { return nil }\n        return .init(\n            value: commentCount,\n            leadingAction: replyAction(appState: appState, commentTreeTracker: commentTreeTracker),\n            trailingAction: nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Interactable/InteractableProviding+Extensions.swift",
    "content": "//\n//  InteractableProviding+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-01-24.\n//\n\nimport MlemMiddleware\n\n// Utility extensions for InteractableProviding\n\nextension InteractableProviding {\n    @MainActor\n    func showReplySheet(commentTreeTracker: CommentTreeTracker? = nil) {\n        if let responseContext {\n            NavigationModel.main.openSheet(.createComment(responseContext, commentTreeTracker: commentTreeTracker))\n        } else {\n            handleError(MlemError.navigationError(\"Cannot open sheet\"), silent: true)\n        }\n    }\n    \n    private var responseContext: CommentEditorView.Context? {\n        if let self = self as? Post { return .post(self) }\n        if let self = self as? Comment { return .comment(self) }\n        return nil\n    }\n    \n    func contextualFlairs() -> Set<PersonFlair> {\n        var output: Set<PersonFlair> = []\n        if creatorIsAdmin.value ?? false {\n            output.insert(.admin)\n        }\n        if creatorIsModerator.value ?? false {\n            output.insert(.moderator)\n        }\n        if let comment = self as? Comment {\n            if let post = comment.post.value_, comment.creatorId == post.creatorId {\n                output.insert(.op)\n            }\n        }\n        return output\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Interactable/InteractableProviding+Toggles.swift",
    "content": "//\n//  InteractableProviding+Toggles.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-02.\n//\n\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\n// Convenience methods for toggling statuses with feedback\n\nextension InteractableProviding {\n    private var inboxItem: (any InboxItemProviding)? { self as? any InboxItemProviding }\n    \n    var toggleVote: ((ScoringOperation, Set<FeedbackType>) -> Void)? {\n        if let updateVote, let votes = votes.value {\n            return { operation, feedback in\n                if feedback.contains(.haptic) {\n                    HapticManager.main.play(haptic: .lightSuccess, tier: .low)\n                }\n                updateVote(operation == votes.myVote ? .none : operation)\n                self.inboxItem?.updateRead(true)\n            }\n        }\n        return nil\n    }\n    \n    var toggleUpvoted: ((Set<FeedbackType>) -> Void)? {\n        if let updateVote, let votes = votes.value {\n            return { feedback in\n                if feedback.contains(.haptic) {\n                    HapticManager.main.play(haptic: .lightSuccess, tier: .low)\n                }\n                updateVote(votes.myVote == .upvote ? .none : .upvote)\n                self.inboxItem?.updateRead(true)\n            }\n        }\n        return nil\n    }\n    \n    var toggleDownvoted: ((Set<FeedbackType>) -> Void)? {\n        if let updateVote, let votes = votes.value {\n            return { feedback in\n                if feedback.contains(.haptic) {\n                    HapticManager.main.play(haptic: .lightSuccess, tier: .low)\n                }\n                updateVote(votes.myVote == .downvote ? .none : .downvote)\n                self.inboxItem?.updateRead(true)\n            }\n        }\n        return nil\n    }\n    \n    var toggleSaved: ((Set<FeedbackType>) -> Void)? {\n        if let saved = saved.value,\n           let votes = votes.value,\n           let updateVote {\n            return { feedback in\n                if feedback.contains(.haptic) {\n                    HapticManager.main.play(haptic: .lightSuccess, tier: .low)\n                }\n                \n                @Setting(\\.behavior_upvoteOnSave) var upvoteOnSave\n                if upvoteOnSave, !saved, votes.myVote != .upvote {\n                    updateVote(.upvote)\n                }\n                self.updateSaved(!saved)\n            }\n        }\n        return nil\n    }\n    \n    func toggleRemoved(reason: String?, feedback: Set<FeedbackType>) {\n        let initialValue = removed\n        if feedback.contains(.haptic) {\n            HapticManager.main.play(haptic: .success, tier: .low)\n        }\n        toggleRemoved(reason: reason) { status in\n            if case .failure = status {\n                ToastModel.main.add(.failure(initialValue ? \"Failed to remove content\" : \"Failed to restore content\"))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Message1Providing+Extensions.swift",
    "content": "//\n//  Message1Providing+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport Haptics\nimport MlemMiddleware\nimport QuickSwipes\n\nextension Message1Providing {\n    var self2: (any Message2Providing)? { self as? any Message2Providing }\n        \n    func swipeActions(notification: InboxNotification?, appState: AppState) -> SwipeConfiguration {\n        .init(\n            trailingActions: {\n                if api.canInteract(appState: appState), !isOwnMessage, let notification {\n                    markReadAction(appState: appState, notification: notification, feedback: [.haptic])\n                }\n            }\n        )\n    }\n    \n    @ActionBuilder\n    func allMenuActions(\n        appState: AppState,\n        feedback: Set<FeedbackType> = [.haptic, .toast],\n        isInMessageFeed: Bool = false,\n        editCallback: (@MainActor () -> Void)?,\n        navigation: NavigationLayer? = nil,\n        notification: InboxNotification? = nil,\n        report: Report? = nil\n    ) -> [any Action] {\n        basicMenuActions(\n            appState: appState,\n            feedback: feedback,\n            isInMessageFeed: isInMessageFeed,\n            editCallback: editCallback,\n            navigation: navigation,\n            notification: notification\n        )\n        if api.isAdmin {\n            ActionGroup(\n                appearance: .init(label: \"Moderation...\", color: .themedModeration, icon: Icons.moderation),\n                displayMode: Settings.get(\\.menus_modActionGrouping) == .divider ? .section : .disclosure\n            ) {\n                moderatorMenuActions(appState: appState, feedback: feedback, report: report)\n            }\n        }\n    }\n        \n    // swiftlint:disable:next cyclomatic_complexity\n    @ActionBuilder func basicMenuActions(\n        appState: AppState,\n        feedback: Set<FeedbackType> = [.haptic, .toast],\n        isInMessageFeed: Bool = false,\n        editCallback: (@MainActor () -> Void)?,\n        navigation: NavigationLayer? = nil,\n        notification: InboxNotification? = nil,\n        report: Report? = nil\n    ) -> [any Action] {\n        if !isOwnMessage {\n            if let navigation, !isInMessageFeed {\n                replyAction(appState: appState, navigation: navigation)\n            }\n            if let notification {\n                markReadAction(appState: appState, notification: notification, feedback: feedback)\n            }\n        }\n        if !deleted {\n            selectTextAction()\n        }\n        if isOwnMessage {\n            if api.supports(.editAndDeletePrivateMessages, defaultValue: true) {\n                if let editCallback {\n                    editAction(appState: appState, callback: editCallback)\n                }\n                if api.supports(.undeletePrivateMessages, defaultValue: true) || !deleted {\n                    deleteAction(appState: appState, feedback: feedback)\n                }\n            }\n        } else {\n            if api.supports(.reportPrivateMessages, defaultValue: true) {\n                if report == nil {\n                    reportAction(appState: appState)\n                }\n            }\n            if !isInMessageFeed {\n                blockCreatorAction(appState: appState, feedback: feedback)\n            }\n        }\n    }\n    \n    @ActionBuilder\n    func moderatorMenuActions(\n        appState: AppState,\n        feedback: Set<FeedbackType> = [.haptic, .toast],\n        report: Report? = nil\n    ) -> [any Action] {\n        if let report {\n            ActionGroup {\n                report.menuActions(appState: appState)\n            }\n        }\n    }\n    \n    func editAction(appState: AppState, callback: @escaping @MainActor () -> Void) -> BasicAction {\n        .init(\n            id: \"edit\\(uid)\",\n            appearance: .edit(),\n            callback: api.canInteract(appState: appState) ? callback : nil\n        )\n    }\n    \n    // These actions are also defined in Interactable1Providing... another protocol for these may be a good idea\n       \n    func replyAction(appState: AppState, navigation: NavigationLayer) -> BasicAction {\n        var callback: (@MainActor () -> Void)?\n        if let creator = creator_, api.canInteract(appState: appState) {\n            callback = { @MainActor in navigation.push(.messageFeed(creator, focusTextField: true)) }\n        }\n        return .init(\n            id: \"reply\\(uid)\",\n            appearance: .reply(),\n            callback: callback\n        )\n    }\n    \n    func blockCreatorAction(appState: AppState, feedback: Set<FeedbackType> = [], showConfirmation: Bool = true) -> BasicAction {\n        .init(\n            id: \"blockCreator\\(uid)\",\n            appearance: .blockCreator(),\n            callback: api.canInteract(appState: appState)\n            ? { @MainActor in self.self2?.creator.toggleBlocked?(feedback, nil) }\n            : nil\n        )\n    }\n\n    func markReadAction(appState: AppState, notification: InboxNotification, feedback: Set<FeedbackType> = []) -> BasicAction {\n        .init(\n            id: \"markRead\\(uid)\",\n            appearance: .markRead(isOn: notification.read),\n            callback: api.canInteract(appState: appState) ? {\n                @MainActor in\n                notification.toggleRead()\n                HapticManager.main.play(haptic: .lightSuccess, tier: .low)\n            } : nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/ModlogEntryContent+Extensions.swift",
    "content": "//\n//  ModlogEntryContent+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-26.\n//\n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nextension ModlogEntryContent {\n    var icon: Icon {\n        switch self {\n        case let .removePost(_, _, removed, _),\n             let .removeComment(_, _, _, _, removed, _),\n             let .removeCommunity(_, removed, _):\n            removed ? .lemmy.remove : .lemmy.restore\n        case let .lockPost(_, _, locked: locked):\n            locked ? .lemmy.addLock : .lemmy.removeLock\n        case let .pinPost(_, _, pinned, _):\n            pinned ? .lemmy.addPin : .lemmy.removePin\n        case .purgePost, .purgeComment, .purgeCommunity, .purgePerson:\n            .lemmy.purge\n        case let .hideCommunity(_, hidden, _):\n            hidden ? .general.hide : .general.show\n        case .transferCommunityOwnership:\n            .lemmy.transferCommunity\n        case let .updatePersonModeratorStatus(_, _, appointed):\n            appointed ? .lemmy.addModerator : .lemmy.removeModerator\n        case .updatePersonAdminStatus:\n            .lemmy.administration\n        case let .banPersonFromCommunity(_, _, banned, _, _):\n            banned ? .lemmy.banFromCommunity : .lemmy.unbanFromCommunity\n        case let .banPersonFromInstance(_, banned, _, _):\n            banned ? .lemmy.banFromInstance : .lemmy.unbanFromInstance\n        }\n    }\n    \n    var color: ThemedColor {\n        switch self {\n        case let .removePost(_, _, removed, _),\n             let .removeComment(_, _, _, _, removed, _),\n             let .removeCommunity(_, removed, _):\n            removed ? .themedNegative : .themedPositive\n        case .lockPost:\n            .themedLockAccent\n        case let .pinPost(_, _, _, type):\n            type == .community ? .themedModeration : .themedAdministration\n        case .purgePost, .purgeComment, .purgeCommunity, .purgePerson:\n            .themedNegative\n        case .hideCommunity:\n            .themedColorfulAccent(4)\n        case .transferCommunityOwnership:\n            .themedColorfulAccent(8)\n        case let .updatePersonModeratorStatus(_, _, appointed):\n            appointed ? .themedModeration : .themedNegative\n        case let .updatePersonAdminStatus(_, appointed):\n            appointed ? .themedAdministration : .themedNegative\n        case let .banPersonFromCommunity(_, _, banned, _, _), let .banPersonFromInstance(_, banned, _, _):\n            banned ? .themedNegative : .themedPositive\n        }\n    }\n    \n    // swiftlint:disable:next cyclomatic_complexity function_body_length\n    func label(userText: Text?) -> LocalizedStringKey {\n        switch self {\n        case let .removePost(_, _, removed, _):\n            if let userText {\n                removed ? \"\\(userText) removed a post\" : \"\\(userText) restored a post\"\n            } else {\n                removed ? \"Post was removed\" : \"Post was restored\"\n            }\n        case let .removeComment(_, _, _, _, removed, _):\n            if let userText {\n                removed ? \"\\(userText) removed a comment\" : \"\\(userText) restored a comment\"\n            } else {\n                removed ? \"Comment was removed\" : \"Comment was restored\"\n            }\n        case let .removeCommunity(_, removed, _):\n            if let userText {\n                removed ? \"\\(userText) removed a community\" : \"\\(userText) restored a community\"\n            } else {\n                removed ? \"Community was removed\" : \"Community was restored\"\n            }\n        case let .lockPost(_, _, locked):\n            if let userText {\n                locked ? \"\\(userText) locked a post\" : \"\\(userText) unlocked a post\"\n            } else {\n                locked ? \"Post was locked\" : \"Post was unlocked\"\n            }\n        case let .pinPost(_, community, pinned, type):\n            pinLabel(userText: userText, community: community, pinned: pinned, type: type)\n        case .purgePost:\n            if let userText {\n                \"\\(userText) purged a post\"\n            } else {\n                \"Post was purged\"\n            }\n        case .purgeComment:\n            if let userText {\n                \"\\(userText) purged a comment\"\n            } else {\n                \"Comment was purged\"\n            }\n        case .purgeCommunity:\n            if let userText {\n                \"\\(userText) purged a community\"\n            } else {\n                \"Community was purged\"\n            }\n        case .purgePerson:\n            if let userText {\n                \"\\(userText) purged a user\"\n            } else {\n                \"User was purged\"\n            }\n        case let .hideCommunity(_, hidden, _):\n            if let userText {\n                hidden ? \"\\(userText) hid a community\" : \"\\(userText) unhid a community\"\n            } else {\n                hidden ? \"Community was hidden\" : \"Community was unhidden\"\n            }\n        case .transferCommunityOwnership:\n            if let userText {\n                \"\\(userText) transferred ownership of a community\"\n            } else {\n                \"Community ownership was transferred\"\n            }\n        case let .updatePersonModeratorStatus(_, _, appointed):\n            if let userText {\n                appointed ? \"\\(userText) appointed a moderator\" : \"\\(userText) removed a moderator\"\n            } else {\n                appointed ? \"Moderator was appointed\" : \"Moderator was removed\"\n            }\n        case let .updatePersonAdminStatus(_, appointed):\n            if let userText {\n                appointed ? \"\\(userText) appointed an administrator\" : \"\\(userText) removed an administrator\"\n            } else {\n                appointed ? \"Administrator was appointed\" : \"Administrator was removed\"\n            }\n        case let .banPersonFromCommunity(_, _, banned, _, _), let .banPersonFromInstance(_, banned, _, _):\n            if let userText {\n                banned ? \"\\(userText) banned a user\" : \"\\(userText) unbanned a user\"\n            } else {\n                banned ? \"User was banned\" : \"User was unbanned\"\n            }\n        }\n    }\n}\n\nprivate func pinLabel(\n    userText: Text?,\n    community: Community,\n    pinned: Bool,\n    type: PostFeatureType\n) -> LocalizedStringKey {\n    let target: String = (type == .community ? community.fullName : community.api.host)\n    if let userText {\n        return pinned ? \"\\(userText) pinned a post to \\(target)\" : \"\\(userText) unpinned a post from \\(target)\"\n    } else {\n        return pinned ? \"Post was pinned to \\(target)\" : \"Post was unpinned from \\(target)\"\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/ModlogEntryType+Extensions.swift",
    "content": "//\n//  ApiModlogActionType+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-11.\n//\n\nimport Foundation\nimport Icons\nimport MlemMiddleware\n\nextension ModlogEntryType {\n    var label: LocalizedStringResource {\n        switch self {\n        case .removePost: \"Remove Post\"\n        case .lockPost: \"Lock Post\"\n        case .pinPost: \"Pin Post\"\n        case .removeComment: \"Remove Comment\"\n        case .removeCommunity: \"Remove Community\"\n        case .banPersonFromCommunity: \"Ban from Community\"\n        case .updatePersonModeratorStatus: \"Appoint Moderator\"\n        case .transferCommunityOwnership: \"Transfer Community\"\n        case .updatePersonAdminStatus: \"Appoint Administrator\"\n        case .banPersonFromInstance: \"Ban from Instance\"\n        case .hideCommunity: \"Hide Community\"\n        case .purgePerson: \"Purge Person\"\n        case .purgeCommunity: \"Purge Community\"\n        case .purgePost: \"Purge Post\"\n        case .purgeComment: \"Purge Comment\"\n        }\n    }\n    \n    var contextualLabel: LocalizedStringResource {\n        switch self {\n        case .removePost, .removeComment, .removeCommunity: \"Remove\"\n        case .lockPost: \"Lock\"\n        case .pinPost: \"Pin\"\n        case .banPersonFromCommunity: \"Ban from Community\"\n        case .updatePersonModeratorStatus: \"Appoint Moderator\"\n        case .transferCommunityOwnership: \"Transfer Ownership\"\n        case .updatePersonAdminStatus: \"Appoint Administrator\"\n        case .banPersonFromInstance: \"Ban from Instance\"\n        case .hideCommunity: \"Hide\"\n        case .purgePerson, .purgeCommunity, .purgePost, .purgeComment: \"Purge\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .removePost, .removeComment, .removeCommunity: .lemmy.remove\n        case .lockPost: .lemmy.addLock\n        case .pinPost: .lemmy.addPin\n        case .banPersonFromCommunity: .lemmy.banFromCommunity\n        case .updatePersonModeratorStatus: .lemmy.moderation\n        case .transferCommunityOwnership: .lemmy.transferCommunity\n        case .updatePersonAdminStatus: .lemmy.administration\n        case .banPersonFromInstance: .lemmy.banFromInstance\n        case .hideCommunity: .general.hide\n        case .purgePerson, .purgeCommunity, .purgePost, .purgeComment: .lemmy.purge\n        }\n    }\n    \n    var appliesToCommunity: Bool {\n        switch self {\n        case .removePost, .lockPost, .pinPost,\n             .removeComment, .banPersonFromCommunity, .updatePersonModeratorStatus,\n             .transferCommunityOwnership, .hideCommunity: true\n        case .removeCommunity, .updatePersonAdminStatus, .banPersonFromInstance,\n             .purgePerson, .purgeCommunity,\n             .purgePost, .purgeComment: false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Person/Person+Actions.swift",
    "content": "//\n//  Person+Actions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-02-06.\n//\n\nimport MlemMiddleware\n\nextension Person {    \n    @MainActor\n    func showBanSheet(community: Community?, isBannedFromCommunity: Bool, shouldBan: Bool) {\n        NavigationModel.main.openSheet(\n            .ban(self, isBannedFromCommunity: isBannedFromCommunity, shouldBan: shouldBan, community: community)\n        )\n    }\n    \n    func banActions(appState: AppState, community: Community?, withUserLabel: Bool = false) -> [any Action] {\n        let canBanFromCommunity: Bool\n        let showBoth: Bool\n        \n        let canBanFromInstance = api.isAdmin && api.supports(.banFromInstance, defaultValue: false)\n        \n        if let myPerson = api.myPerson, let community, let myPersonModerates = myPerson.moderates {\n            let supportedByApi = api.supports(.banFromCommunity, defaultValue: false) && (\n                apiIsLocal || api.supports(.banFromNonLocalCommunity, defaultValue: false)\n            )\n            canBanFromCommunity = myPersonModerates(.id(community.id)) && supportedByApi\n            showBoth = canBanFromInstance && isBannedFromCommunity(community) != bannedFromInstance\n        } else {\n            canBanFromCommunity = false\n            showBoth = false\n        }\n        var output: [any Action] = .init()\n        // admins should see separate 'ban' and 'unban' actions if ban statuses conflict; otherwise actions are grouped under a single entry (community or instance, depending on moderation status)\n        // moderators see community ban action by default, regardless of admin status\n        if canBanFromCommunity {\n            if showBoth {\n                output.append(banFromInstanceAction(appState: appState))\n            }\n            if let community {\n                output.append(banFromCommunityAction(appState: appState, community: community, withUserLabel: withUserLabel))\n            }\n        }\n        // non-moderator admins see instance ban action by default\n        else if canBanFromInstance {\n            output.append(banFromInstanceAction(appState: appState))\n            if showBoth, let community {\n                output.append(banFromCommunityAction(appState: appState, community: community, withUserLabel: withUserLabel))\n            }\n        }\n        return output\n    }\n    \n    func banFromInstanceAction(appState: AppState, withUserLabel: Bool = false) -> BasicAction {\n        .init(\n            id: \"banFromInstance\\(uid)\",\n            appearance: .banFromInstance(isOn: bannedFromInstance, withUserLabel: withUserLabel),\n            callback: api.canInteract(appState: appState) && api.isAdmin ? { @MainActor in\n                self.showBanSheet(\n                    community: nil,\n                    isBannedFromCommunity: false,\n                    shouldBan: !self.bannedFromInstance\n                )\n            } : nil\n        )\n    }\n    \n    func banFromCommunityAction(appState: AppState, community: Community, withUserLabel: Bool = false) -> BasicAction {\n        let isBanned = isBannedFromCommunity(community)\n        let callback: (@MainActor () -> Void)?\n        if let isBanned, api.canInteract(appState: appState), community.canModerate {\n            callback = {\n                self.showBanSheet(\n                    community: community,\n                    isBannedFromCommunity: isBanned,\n                    shouldBan: !isBanned\n                )\n            }\n        } else {\n            callback = nil\n        }\n        \n        return .init(\n            id: \"banFromCommunity\\(uid)\",\n            appearance: .banFromCommunity(isOn: isBanned ?? false, withUserLabel: withUserLabel),\n            callback: callback\n        )\n    }\n    \n    /// Action to add/remove admin\n    /// - Parameters:\n    ///   - instance: instance to add the admin to\n    ///   - isOn: true if the user is already an admin, false otherwise\n    func addAdminAction(instance: Instance, isOn: Bool) -> BasicAction {\n        let callback: (@MainActor () -> Void) = {\n            instance.addAdmin(personId: self.id, added: !isOn)\n        }\n        \n        return .init(\n            id: \"addAdmin\\(uid)\",\n            appearance: .addAdmin(isOn: isOn),\n            confirmationPrompt: isOn\n                ? \"Really remove administrator \\(displayName) from \\(instance.displayName)?\"\n                : \"Really appoint \\(displayName) as an administrator of \\(instance.displayName)?\",\n            callback: callback\n        )\n    }\n    \n    func addModAction(community: Community, isOn: Bool) -> BasicAction {\n        let callback: (@MainActor () -> Void) = {\n            Task {\n                do {\n                    try await community.addModerator(self, added: !isOn)\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n        \n        return .init(\n            id: \"addMod\\(uid)\",\n            appearance: .addMod(isOn: isOn),\n            confirmationPrompt: isOn\n                ? \"Really remove moderator \\(displayName) from \\(community.displayName)?\"\n                : \"Really appoint \\(displayName) as a moderator of \\(community.displayName)?\",\n            callback: callback\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Person/Person+Extensions.swift",
    "content": "//\n//  Person+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-02-06.\n//\n\nimport MlemBackend\nimport MlemMiddleware\n\nextension Person {\n    var shouldHideInFeed: Bool { blocked_.realizedValue || purged }\n\n    var isMlemDeveloper: Bool {\n        BackendClient.main.flairs.developers.contains(actorId.description)\n    }\n    \n    func flairs(\n        interactableContext interactable: (any InteractableProviding)? = nil,\n        communityContext community: Community? = nil\n    ) -> [PersonFlair] {\n        @Setting(\\.person_ageVisibility) var alwaysShowAccountAge\n        \n        let community = community ?? interactable?.community.value\n        var output: Set<PersonFlair> = []\n        \n        if isMlemDeveloper {\n            output.insert(.developer)\n        }\n        if isBot {\n            output.insert(.bot)\n        }\n        if bannedFromInstance {\n            output.insert(.bannedFromInstance)\n        }\n        if let community, isBannedFromCommunity(community) ?? false {\n            output.insert(.bannedFromCommunity)\n        }\n        \n        if (alwaysShowAccountAge == .newAccountsOnly && createdRecently) || alwaysShowAccountAge == .always {\n            output.insert(.accountAge(created))\n        } else if isCakeDay {\n            output.insert(.cakeDay)\n        }\n        \n        if let interactable {\n            if let creator = interactable.creator.value {\n                assert(creator.actorId == actorId)\n            } else {\n                assertionFailure(\"No creator!\")\n            }\n            output.formUnion(interactable.contextualFlairs())\n        } else {\n            if api.myInstance?.administrators.value?.contains(where: { $0.id == id }) ?? false {\n                output.insert(.admin)\n            }\n        }\n        \n        if let community, community.moderators.value?.contains(where: { $0.id == id }) ?? false {\n            output.insert(.moderator)\n        }\n        \n        return output.sorted { $0.sortVal < $1.sortVal }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/PersonContent+Extensions.swift",
    "content": "//\n//  PersonContent+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-10-31.\n//\n\nimport MlemMiddleware\n\nextension PersonContent {\n    var shouldHideInFeed: Bool {\n        switch wrappedValue {\n        case let .post(post): post.shouldHideInFeed\n        case let .comment(comment): comment.shouldHideInFeed\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Post/Post+Actions.swift",
    "content": "//\n//  Post+Actions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-01-03.\n//\n\nimport MlemMiddleware\nimport Foundation\nimport os\n\n// swiftlint:disable file_length\n\n// Functions to support the old Action system\n\nextension Post {\n    func hideAction(appState: AppState, feedback: Set<FeedbackType>) -> BasicAction? {\n        guard let hidden = hidden.value, let toggleHidden = toggleHidden else { return nil }\n        return .init(\n            id: \"hide\\(uid)\",\n            appearance: .hide(isOn: hidden),\n            callback: api.supports(.hidePosts, defaultValue: true) && api.canInteract(appState: appState)\n            ? { @MainActor in toggleHidden(feedback) }\n            : nil\n        )\n    }\n    \n    func blockAction(appState: AppState, feedback: Set<FeedbackType>) -> ActionGroup {\n        .init(\n            appearance: .init(\n                label: \"Block...\",\n                isDestructive: true,\n                color: .themedNegative,\n                icon: Icons.block\n            ),\n            prompt: \"Block community or user?\",\n            disabled: !api.canInteract(appState: appState),\n            displayMode: .popup\n        ) {\n            if let blockCreatorAction = blockCreatorAction(appState: appState, feedback: feedback, showConfirmation: false) {\n                blockCreatorAction\n            }\n            if let blockCommunityAction = blockCommunityAction(appState: appState, feedback: feedback, showConfirmation: false) {\n                blockCommunityAction\n            }\n        }\n    }\n    \n    func blockCommunityAction(appState: AppState, feedback: Set<FeedbackType> = [], showConfirmation: Bool = true) -> BasicAction? {\n        guard let community = community.value,\n              let toggleBlocked = community.toggleBlocked else { return nil }\n        return .init(\n            id: \"blockCommunity\\(actorId.description)\",\n            appearance: .init(\n                label: \"Block Community\",\n                isOn: false,\n                isDestructive: true,\n                color: .themedNegative,\n                icon: Icons.block\n            ),\n            confirmationPrompt: showConfirmation ? \"Really block this community?\" : nil,\n            callback: api.canInteract(appState: appState)\n            ? { @MainActor in toggleBlocked(feedback, nil) }\n            : nil\n        )\n    }\n    \n    func crossPostAction() -> BasicAction {\n        .init(\n            id: \"crosspost\\(uid)\",\n            appearance: .crossPost(),\n            callback: {\n                var crossPostContent: String\n                let crossPostedLabel = String(localized: \"Crossposted from \\(self.actorId.description)\")\n                if let content = self.content, !content.isEmpty {\n                    crossPostContent = \"\\(crossPostedLabel)\\n-----\\n\\(content)\"\n                } else {\n                    crossPostContent = crossPostedLabel\n                }\n                NavigationModel.main.openSheet(.createPost(\n                    community: nil,\n                    title: self.title,\n                    content: crossPostContent,\n                    type: self.type,\n                    nsfw: self.nsfw,\n                    feedLoader: .init(wrappedValue: nil)\n                ))\n            }\n        )\n    }\n    \n    func lockAction(appState: AppState, feedback: Set<FeedbackType> = []) -> BasicAction? {\n        guard api.canInteract(appState: appState) && canModerate else { return nil }\n        return .init(\n            id: \"lock\\(uid)\",\n            appearance: .lock(isOn: locked, isInProgress: lockedPending),\n            confirmationPrompt: locked ? \"Really unlock this post?\" : \"Really lock this post?\",\n            callback: { self.toggleLocked(feedback) }\n        )\n    }\n    \n    func pinAction(appState: AppState, feedback: Set<FeedbackType> = []) -> ActionGroup {\n        .init(\n            appearance: .pin(isOn: false, isInProgress: pinnedCommunityPending || pinnedInstancePending),\n            prompt: \"Pin to Community or Instance?\",\n            displayMode: .popup\n        ) {\n            pinToCommunityAction(appState: appState, feedback: feedback, showConfirmation: false)\n            pinToInstanceAction(appState: appState, feedback: feedback, showConfirmation: false)\n        }\n    }\n    \n    func pinToCommunityAction(\n        appState: AppState,\n        feedback: Set<FeedbackType> = [],\n        verboseTitle: Bool = true,\n        showConfirmation: Bool = true\n    ) -> BasicAction {\n        let isOn = pinnedCommunity\n        let prompt: LocalizedStringResource?\n        if showConfirmation {\n            if let communityName = community.value?.name {\n                if isOn {\n                    prompt = \"Really unpin this post from \\(communityName)?\"\n                } else {\n                    prompt = \"Really pin this post to \\(communityName)?\"\n                }\n            } else {\n                if isOn {\n                    prompt = \"Really unpin this post from the community?\"\n                } else {\n                    prompt = \"Really pin this post to the community?\"\n                }\n            }\n        } else {\n            prompt = nil\n        }\n        return .init(\n            id: \"pinToCommunity\\(uid)\",\n            appearance: verboseTitle ? .pinToCommunity(\n                isOn: isOn, isInProgress: pinnedCommunityPending\n            ) : .pin(\n                isOn: isOn, isInProgress: pinnedCommunityPending\n            ),\n            confirmationPrompt: prompt,\n            callback: api.canInteract(appState: appState) && canModerate ? { @MainActor in\n                self.togglePinnedCommunity(feedback: feedback)\n            } : nil\n        )\n    }\n    \n    func pinToInstanceAction(appState: AppState, feedback: Set<FeedbackType> = [], showConfirmation: Bool = true) -> BasicAction {\n        let isOn = pinnedInstance\n        let prompt: LocalizedStringResource?\n        if showConfirmation {\n            if isOn {\n                prompt = \"Really unpin this post from \\(host)?\"\n            } else {\n                prompt = \"Really pin this post to \\(host)?\"\n            }\n        } else {\n            prompt = nil\n        }\n        return .init(\n            id: \"pinToInstance\\(uid)\",\n            appearance: .pinToInstance(isOn: isOn, isInProgress: pinnedInstancePending),\n            confirmationPrompt: prompt,\n            callback: api.canInteract(appState: appState) && api.isAdmin ? { @MainActor in\n                self.togglePinnedInstance(feedback: feedback)\n            } : nil\n        )\n    }\n    \n    func createImageAction(navigation: NavigationLayer) -> BasicAction {\n        .init(\n            id: \"exportAsImage\\(uid)\",\n            appearance: .createImage()) {\n                navigation.openSheet(.exportPostImage(self))\n            }\n    }\n    \n    func editAction(appState: AppState, navigation: NavigationLayer) -> BasicAction? {\n        guard api.canInteract(appState: appState) else { return nil }\n        return .init(\n            id: \"edit\\(uid)\",\n            appearance: .edit(),\n            callback: { navigation.openSheet(.editPost(self)) }\n        )\n    }\n    \n    func setNsfwAction(appState: AppState) -> BasicAction? {\n        guard setNsfwIsAvailable(appState: appState) else { return nil }\n        return .init(\n            id: \"setNsfw\\(uid)\",\n            appearance: .toggleNsfw(isOn: nsfw),\n            callback: { @MainActor in\n                self.toggleNsfw { status in\n                    Task {\n                        await self.handleModerationActionCompletion(\n                            message: \"Failed to set NSFW status\",\n                            result: status,\n                            feedback: [.haptic]\n                        )\n                    }\n                }\n            }\n        )\n    }\n    \n    func setNsfwIsAvailable(appState: AppState) -> Bool {\n        guard let community = community.value else { return false }\n        guard community.apiIsLocal else { return false }\n        guard canModerate else { return false }\n        guard api.canInteract(appState: appState) else { return false }\n        guard api.supports(.moderatorSetNsfw, defaultValue: false) else { return false }\n        return true\n    }\n    \n    func viewVotesAction(navigation: NavigationLayer) -> BasicAction? {\n        guard canModerate && api.supports(.viewVotes, defaultValue: true) else { return nil }\n        return .init(\n            id: \"viewVotes\\(uid)\",\n            appearance: .viewVotes(),\n            callback: { @MainActor in navigation.push(.votesList(.post(self))) }\n        )\n    }\n    \n    // swiftlint:disable:next cyclomatic_complexity\n    func action(\n        appState: AppState,\n        navigation: NavigationLayer,\n        type: PostBarConfiguration.ActionType,\n        feedback: Set<FeedbackType> = [.haptic, .toast],\n        commentTreeTracker: CommentTreeTracker? = nil,\n        communityContext: Community? = nil,\n        reportContext: Report? = nil\n    ) -> (any Action)? {\n        switch type {\n        case .upvote: return upvoteAction(appState: appState, feedback: feedback)\n        case .downvote: return downvoteAction(appState: appState, feedback: feedback)\n        case .save: return saveAction(appState: appState, feedback: feedback)\n        case .reply: return replyAction(appState: appState, commentTreeTracker: commentTreeTracker)\n        case .share: return shareAction(navigation: navigation)\n        case .selectText: return selectTextAction()\n        case .hide: return hideAction(appState: appState, feedback: feedback)\n        case .block: return blockAction(appState: appState, feedback: feedback)\n        case .report: return reportAction(appState: appState, communityContext: communityContext)\n        case .crossPost: return crossPostAction()\n        case .lock: return lockAction(appState: appState, feedback: feedback)\n        case .pin: return api.isAdmin ? pinAction(\n                appState: appState,\n                feedback: feedback\n            ) : pinToCommunityAction(\n                appState: appState,\n                feedback: feedback\n            )\n        case .resolve: return reportContext?.resolveAction(appState: appState, feedback: feedback)\n        case .remove: return removeAction(appState: appState, feedback: feedback)\n        case .ban: return reportContext?.contextualBanAction(appState: appState)\n        }\n    }\n    \n    // MARK: - Readouts\n    \n    func upvoteReadout(showColor: Bool) -> Readout? {\n        if let votes = votes.value {\n            let isOn = votes.myVote == .upvote\n            return Readout(\n                id: \"upvote\\(actorId)\",\n                label: votes.upvotes.description,\n                icon: isOn ? Icons.upvoteSquareFill : Icons.upvoteSquare,\n                color: isOn && showColor ? .themedUpvote : nil\n            )\n        }\n        return nil\n    }\n    \n    func downvoteReadout(showColor: Bool) -> Readout? {\n        if let votes = votes.value {\n            let isOn = votes.myVote == .downvote\n            return Readout(\n                id: \"downvote\\(actorId)\",\n                label: votes.downvotes.description,\n                icon: isOn ? Icons.downvoteSquareFill : Icons.downvoteSquare,\n                color: isOn && showColor ? .themedDownvote : nil\n            )\n        }\n        return nil\n    }\n    \n    func readout(type: PostBarConfiguration.ReadoutType, showColor: Bool) -> Readout? {\n        switch type {\n        case .created: createdReadout\n        // swiftlint:disable:next void_function_in_ternary\n        case .score: downvotesEnabled ? scoreReadout(showColor: showColor) : upvoteReadout(showColor: showColor)\n        case .upvote: upvoteReadout(showColor: showColor)\n        case .downvote: downvotesEnabled ? downvoteReadout(showColor: showColor) : nil\n        case .comment: commentReadout\n        case .saved: savedReadout(showColor: showColor)\n        }\n    }\n    \n    // MARK: - Counters\n    \n    func counter(\n        appState: AppState,\n        type: PostBarConfiguration.CounterType,\n        commentTreeTracker: CommentTreeTracker? = nil\n    ) -> Counter? {\n        switch type {\n        case .score: scoreCounter(appState: appState, downvotesEnabled: downvotesEnabled)\n        case .upvote: upvoteCounter(appState: appState)\n        case .downvote: downvotesEnabled ? downvoteCounter(appState: appState, downvotesEnabled: downvotesEnabled) : nil\n        case .reply: replyCounter(appState: appState, commentTreeTracker: commentTreeTracker)\n        }\n    }\n    \n    // MARK: - Action Groups\n    \n    @ActionBuilder\n    func allMenuActions(\n        appState: AppState,\n        expanded: Bool = false,\n        feedback: Set<FeedbackType> = [.haptic, .toast],\n        showAllActions: Bool = true,\n        navigation: NavigationLayer?,\n        report: Report? = nil,\n        commentTreeTracker: CommentTreeTracker? = nil\n    ) -> [any Action] {\n        basicMenuActions(\n            appState: appState,\n            expanded: expanded,\n            feedback: feedback,\n            navigation: navigation,\n            commentTreeTracker: commentTreeTracker\n        )\n        if canModerate {\n            ActionGroup(\n                appearance: .init(label: \"Moderation...\", color: .themedModeration, icon: Icons.moderation),\n                displayMode: Settings.get(\\.menus_modActionGrouping) == .divider || expanded ? .section : .disclosure\n            ) {\n                moderatorMenuActions(\n                    appState: appState,\n                    feedback: feedback,\n                    showAllActions: showAllActions,\n                    navigation: navigation,\n                    report: report\n                )\n            }\n        }\n    }\n    \n    @ActionBuilder\n    func basicMenuActions(\n        appState: AppState,\n        expanded: Bool,\n        feedback: Set<FeedbackType> = [.haptic, .toast],\n        navigation: NavigationLayer?,\n        commentTreeTracker: CommentTreeTracker? = nil\n    ) -> [any Action] {\n        ActionGroup(displayMode: .compactSection) {\n            if let upvoteAction = upvoteAction(appState: appState, feedback: feedback) { upvoteAction }\n            if let downvoteAction = downvoteAction(appState: appState, feedback: feedback) { downvoteAction }\n            if let saveAction = saveAction(appState: appState, feedback: feedback) { saveAction }\n            replyAction(appState: appState, commentTreeTracker: commentTreeTracker)\n            if !deleted {\n                selectTextAction()\n            }\n            shareAction(navigation: navigation)\n            \n            if expanded, let navigation {\n                createImageAction(navigation: navigation)\n            }\n            \n            if isOwnPost, let navigation, let editAction = editAction(appState: appState, navigation: navigation) {\n                editAction\n                deleteAction(appState: appState, feedback: feedback)\n            } else {\n                if api.supports(.hidePosts, defaultValue: true),\n                let hideAction = hideAction(appState: appState, feedback: feedback) {\n                    hideAction\n                }\n                if !canModerate, !deleted {\n                    reportAction(appState: appState)\n                }\n                blockAction(appState: appState, feedback: feedback)\n            }\n        }\n    }\n    \n    @ActionBuilder\n    // swiftlint:disable:next cyclomatic_complexity\n    func moderatorMenuActions(\n        appState: AppState,\n        feedback: Set<FeedbackType> = [.haptic, .toast],\n        showAllActions: Bool = true,\n        navigation: NavigationLayer?,\n        report: Report? = nil\n    ) -> [any Action] {\n        if showAllActions || Settings.get(\\.menus_allModActions) {\n            pinToCommunityAction(appState: appState, feedback: feedback, verboseTitle: api.isAdmin)\n            if api.isAdmin {\n                pinToInstanceAction(appState: appState, feedback: feedback)\n            }\n            if let lockAction = lockAction(appState: appState, feedback: feedback) { lockAction }\n            \n            if setNsfwIsAvailable(appState: appState),\n               let setNsfwAction = setNsfwAction(appState: appState) {\n                setNsfwAction\n            }\n            \n            if let navigation,\n               api.supports(.viewVotes, defaultValue: false),\n               let viewVotesAction = viewVotesAction(navigation: navigation) {\n                viewVotesAction\n            }\n        }\n        if !isOwnPost {\n            if canModerate { removeAction(appState: appState) }\n            if let creator = creator.value, let community = community.value {\n                creator.banActions(appState: appState, community: community, withUserLabel: true)\n            }\n        }\n        if api.isAdmin, api.supports(.purgeContent, defaultValue: false) {\n            purgeAction(appState: appState)\n            if !isOwnPost, let purgeCreatorAction = purgeCreatorAction(appState: appState) {\n                purgeCreatorAction\n            }\n        }\n        if let report {\n            ActionGroup {\n                report.menuActions(appState: appState)\n            }\n        }\n    }\n}\n\n// swiftlint:enable file_length\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Post/Post+Extensions.swift",
    "content": "//\n//  Post+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-01-07.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension Post {\n    func shouldShowLoadingSymbol(for barConfiguration: PostBarConfiguration? = nil) -> Bool {\n        if lockedPending, !(barConfiguration?.all.contains(.action(.lock)) ?? false) {\n            return true\n        }\n        if pinnedCommunityPending, !(barConfiguration?.all.contains(.action(.pin)) ?? false) {\n            return true\n        }\n        if pinnedInstancePending, !(barConfiguration?.all.contains(.action(.pin)) ?? false) {\n            return true\n        }\n        if nsfwPending {\n            return true\n        }\n        return false\n    }\n    \n    var shouldHideInFeed: Bool {\n        (creator.value_?.shouldHideInFeed ?? false) || (community.value_?.shouldHideInFeed ?? false) || (hidden.value_ ?? false) || purged\n    }\n    \n    func taggedTitle(communityContext: Community?) -> Text {\n        let hasTags: Bool = removed\n            || deleted\n            || pinnedInstance\n            || (communityContext != nil && pinnedCommunity)\n            || locked\n        \n        return postTag(active: removed, icon: .lemmy.removed, color: .themedNegative) +\n            postTag(active: deleted, icon: .general.delete, color: .themedNegative) +\n            postTag(active: pinnedInstance, icon: .lemmy.pinned, color: .themedAdministration) +\n            postTag(active: pinnedCommunity && communityContext != nil, icon: .lemmy.pinned, color: .themedModeration) +\n            postTag(active: locked, icon: .lemmy.locked, color: .themedLockAccent) +\n            Text(verbatim: \"\\(hasTags ? \"  \" : \"\")\\(title)\")\n    }\n    \n    var imageFallback: MediaView.Fallback {\n        switch type {\n        case .text: .text\n        case let .media(url), let .embedded(url, _):\n            url.proxyAwarePathExtension?.isMovieExtension ?? false ? .movie : .image\n        case .link: .link\n        case .poll: .poll\n        case .titleOnly: .titleOnly\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Post/Post+Toggles.swift",
    "content": "//\n//  Post+Toggles.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-01-04.\n//\n\nimport MlemMiddleware\nimport Haptics\nimport os\nimport Foundation\n\n// Convenience methods for toggling statuses with feedback\n\nextension Post {\n    var toggleHidden: ((Set<FeedbackType>) -> Void)? {\n        guard let hidden = hidden.value else { return nil }\n        return { feedback in\n            if feedback.contains(.haptic) {\n                HapticManager.main.play(haptic: .lightSuccess, tier: .low)\n            }\n            if feedback.contains(.toast) {\n                if hidden {\n                    ToastModel.main.add(.success(\"Shown\"))\n                } else {\n                    ToastModel.main.add(\n                        .undoable(\n                            \"Hidden\",\n                            icon: .general.hide,\n                            callback: { self.updateHidden(false) }\n                        )\n                    )\n                }\n            }\n            self.updateHidden(!hidden)\n        }\n    }\n    \n    func togglePinnedCommunity(feedback: Set<FeedbackType>) {\n        let shouldPin = !pinnedCommunity\n        togglePinnedCommunity { status in\n            Task {\n                await self.handleModerationActionCompletion(\n                    message: shouldPin ? \"Failed to pin post\" : \"Failed to unpin post\",\n                    result: status,\n                    feedback: feedback\n                )\n            }\n        }\n    }\n    \n    func toggleLocked(_ feedback: Set<FeedbackType>, callback: ((UpdateStatus) -> Void)? = nil) {\n        if feedback.contains(.haptic) {\n            HapticManager.main.play(haptic: .lightSuccess, tier: .low)\n        }\n        updateLocked(!locked, callback: callback)\n    }\n    \n    /// Toggles the community pinned status of this post\n    /// - Parameter callback: if present, when the repository call completes, is called with `.success` if the operation succeeded and `.failure` otherwise.\n    func togglePinnedCommunity(callback: ((UpdateStatus) -> Void)? = nil) {\n        updatePinnedCommunity(!pinnedCommunity, callback: callback)\n    }\n    \n    func togglePinnedInstance(feedback: Set<FeedbackType>) {\n        let shouldPin = !pinnedInstance\n        togglePinnedInstance { status in\n            Task {\n                await self.handleModerationActionCompletion(\n                    message: shouldPin ? \"Failed to pin post\" : \"Failed to unpin post\",\n                    result: status,\n                    feedback: feedback\n                )\n            }\n        }\n    }\n    \n    /// Toggles the instance pinned status of this post\n    /// - Parameter callback: if present, when the repository call completes, is called with `.success` if the operation succeeded and `.failure` otherwise.\n    func togglePinnedInstance(callback: ((UpdateStatus) -> Void)? = nil) {\n        updatePinnedInstance(!pinnedInstance, callback: callback)\n    }\n    \n    func toggleNsfw(callback: ((UpdateStatus) -> Void)?) {\n        updateNsfw(!nsfw, callback: callback)\n    }\n    \n    // MARK: - Helpers\n    \n    // TODO: UpdateQueue remove this shim code\n    internal func handleModerationActionCompletion(\n        message: LocalizedStringResource,\n        result: UpdateStatus,\n        feedback: Set<FeedbackType>\n    ) async {\n        var stateUpdateResult: StateUpdateResult\n        switch result {\n        case .success:\n            stateUpdateResult = .succeeded\n        case .failure:\n            stateUpdateResult = .failed\n        }\n        await handleModerationActionCompletion(message: message, result: stateUpdateResult, feedback: feedback)\n    }\n    \n    internal func handleModerationActionCompletion(\n        message: LocalizedStringResource,\n        result: StateUpdateResult,\n        feedback: Set<FeedbackType>\n    ) async {\n        if feedback.contains(.haptic) {\n            HapticManager.main.play(haptic: .success, tier: .low)\n        }\n        switch result {\n        case .failed:\n            ToastModel.main.add(.failure(message))\n        default:\n            break\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/ProfileProviding+Extensions.swift",
    "content": "//\n//  ProfileProviding+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-13.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nextension ProfileProviding {\n    var isCakeDay: Bool { profileCreated?.isAnniversaryToday ?? false }\n    \n    var createdRecently: Bool {\n        guard let created = profileCreated else { return false }\n        let intervalSinceCreation = Date.now.timeIntervalSince(created)\n        return intervalSinceCreation < 30 * 24 * 60 * 60\n    }\n    \n    static var avatarFallback: MediaView.Fallback {\n        if self is Community.Type {\n            return .communityAvatar\n        } else if self is Instance.Type {\n            return .instanceAvatar\n        } else if self is Person.Type || self is any Account.Type {\n            return .personAvatar\n        } else {\n            assertionFailure()\n            return .personAvatar\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/PurgableProviding+Extensions.swift",
    "content": "//\n//  PurgableProviding+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-10-27.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nextension PurgableProviding {\n    @MainActor\n    func showPurgeSheet() {\n        NavigationModel.main.openSheet(.purge(self))\n    }\n    \n    func purgeAction(appState: AppState) -> BasicAction {\n        .init(\n            id: \"purge\\(uid)\",\n            appearance: .purge(),\n            callback: (api.canInteract(appState: appState) && api.isAdmin) ? showPurgeSheet : nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/RegistrationApplication+Extensions.swift",
    "content": "//\n//  RegistrationApplication+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-14.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nextension RegistrationApplication {\n    @MainActor\n    func showDenialSheet() {\n        NavigationModel.main.openSheet(.denyApplication(self))\n    }\n    \n    @ActionBuilder\n    func menuActions() -> [any Action] {\n        if resolution != .approved {\n            approveAction()\n        }\n        if !resolution.isDenied {\n            denyAction()\n        }\n    }\n    \n    func approveAction() -> BasicAction {\n        .init(\n            id: \"approveApplication\\(id)\",\n            appearance: .init(\n                label: \"Approve\",\n                color: .themedPositive,\n                icon: Icons.successCircle\n            ),\n            callback: { self.approve() }\n        )\n    }\n    \n    func denyAction() -> BasicAction {\n        .init(\n            id: \"denyApplication\\(id)\",\n            appearance: .init(\n                label: \"Deny\",\n                color: .themedNegative,\n                icon: Icons.failureCircle\n            ),\n            callback: showDenialSheet\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/RemovableProviding+Extensions.swift",
    "content": "//\n//  RemovableProviding+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-15.\n//\n\nimport MlemMiddleware\n\nextension RemovableProviding {\n    @MainActor\n    func showRemoveSheet() {\n        NavigationModel.main.openSheet(.remove(self))\n    }\n    \n    func removeAction(appState: AppState, feedback: Set<FeedbackType> = []) -> BasicAction {\n        .init(\n            id: \"remove\\(uid)\",\n            appearance: .remove(isOn: removed, isInProgress: removedPending),\n            callback: api.canInteract(appState: appState) ? showRemoveSheet : nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Reply1Providing+Extensions.swift",
    "content": ""
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Report+Extensions.swift",
    "content": "//\n//  Report+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-16.\n//\n\nimport Haptics\nimport MlemMiddleware\n\nextension Report {\n    func toggleResolved(feedback: Set<FeedbackType>) {\n        if feedback.contains(.haptic) {\n            HapticManager.main.play(haptic: .success, tier: .low)\n        }\n        toggleResolved()\n    }\n    \n    @ActionBuilder\n    func menuActions(\n        appState: AppState,\n        feedback: Set<FeedbackType> = [.haptic]\n    ) -> [any Action] {\n        resolveAction(appState: appState, feedback: feedback)\n    }\n    \n    func resolveAction(appState: AppState, feedback: Set<FeedbackType> = []) -> BasicAction {\n        .init(\n            id: \"resolve\\(cacheId)\",\n            appearance: .resolve(isOn: resolved),\n            callback: api.canInteract(appState: appState) ? { @MainActor in self.toggleResolved(feedback: feedback) } : nil\n        )\n    }\n    \n    func contextualBanAction(appState: AppState) -> BasicAction? {\n        guard let myPerson = api.myPerson,\n              let myPersonModerates = myPerson.moderates else { return nil }\n        \n        if let community = target.community, let creator = target.creator.value, myPersonModerates(.id(community.id)) {\n            return creator.banFromCommunityAction(appState: appState, community: community)\n        }\n        \n        return target.creator.value?.banFromInstanceAction(appState: appState)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/ReportableProviding+Extensions.swift",
    "content": "//\n//  ReportableProviding+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 12/08/2024.\n//\n\nimport MlemMiddleware\n\nextension ReportableProviding {\n    @MainActor\n    func showReportSheet(communityContext: Community? = nil) {\n        NavigationModel.main.openSheet(.report(self, community: communityContext))\n    }\n    \n    func reportAction(appState: AppState, communityContext: Community? = nil) -> BasicAction {\n        .init(\n            id: \"report\\(uid)\",\n            appearance: .report(),\n            callback: api.canInteract(appState: appState) ? { @MainActor in self.showReportSheet(communityContext: communityContext) } : nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/ScoringOperation+Extensions.swift",
    "content": "//\n//  ScoringOperation+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-18.\n//\n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nextension ScoringOperation {\n    var systemImage: String {\n        switch self {\n        case .none: Icons.resetVoteSquare\n        case .upvote: Icons.upvoteSquare\n        case .downvote: Icons.downvoteSquare\n        }\n    }\n\n    var icon: Icon {\n        switch self {\n        case .none: .lemmy.removeUpvote\n        case .upvote: .lemmy.addUpvote\n        case .downvote: .lemmy.addDownvote\n        }\n    }\n    \n    var color: ThemedColor {\n        switch self {\n        case .none: .themedSecondary\n        case .upvote: .themedUpvote\n        case .downvote: .themedDownvote\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/SelectableContentProviding+Extensions.swift",
    "content": "//\n//  SelectableContentProviding+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 02/07/2024.\n//\n\nimport MlemMiddleware\n\nextension SelectableContentProviding {\n    @MainActor\n    func showTextSelectionSheet() {\n        NavigationModel.main.openSheet(.selectText(selectableContent ?? \"\"))\n    }\n    \n    func selectTextAction() -> BasicAction {\n        .init(\n            id: \"selectText\\(actorId.description)\",\n            appearance: .selectText(),\n            callback: selectableContent == nil ? nil : showTextSelectionSheet\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/Sharable+Extensions.swift",
    "content": "//\n//  Sharable+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-09.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nextension Sharable {\n    var lemmyverseUrl: URL? {\n        (URL(string: \"https://lemmyverse.link/\")?\n            .appendingPathComponent(actorId.host)\n            .appendingPathComponent(actorId.url.path()))\n    }\n    \n    func shareAction(navigation: NavigationLayer?) -> BasicAction {\n        .init(id: \"share\\(actorId)\", appearance: .share(), callback: {\n            let url: URL? = switch Settings.get(\\.links_shareMode) {\n            case .myInstance: self.url()\n            case .originalInstance: self.actorId.url\n            case .lemmyverse: self.lemmyverseUrl\n            case .askEveryTime: nil\n            }\n            if let url, let navigation {\n                navigation.model?.shareInfo = .init(url: url, actions: self.shareSheetActions())\n            } else {\n                navigation?.openSheet(.shareInstancePicker(self))\n            }\n        })\n    }\n    \n    func shareSheetActions() -> [BasicAction] {\n        var shareActions: [BasicAction] = [sendLinkInPrivateMessageAction()]\n        if let post = self as? Post {\n            shareActions.prepend(post.crossPostAction())\n        }\n        return shareActions\n    }\n        \n    func sendLinkInPrivateMessageAction() -> BasicAction {\n        .init(\n            id: \"sendLinkInPrivateMessage\\(actorId)\",\n            appearance: .init(\n                label: \"Send to Lemmy User\",\n                color: .themedAccent,\n                icon: Icons.personCircle\n            ),\n            callback: {\n                NavigationModel.main.openSheet(.personPicker(callback: { person, navigation in\n                    navigation.push(\n                        .messageFeed(person, messageContent: String(describing: self.actorId), focusTextField: true)\n                    )\n                }))\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/SiteSoftware+Extensions.swift",
    "content": "//\n//  SiteSoftware+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-06-14.\n//\n\nimport Foundation\nimport MlemBackend\nimport MlemMiddleware\n\nextension SiteSoftware {\n    init(from software: InstanceSummarySoftware) {\n        let type: SiteSoftwareType = switch software.type {\n        case .lemmy: .lemmy\n        case .pieFed: .pieFed\n        }\n        \n        let version: SiteVersion = .init(software.version)\n        self.init(type: type, version: version)\n    }\n    \n    var label: String {\n        \"\\(String(localized: type.label)) \\(version)\"\n    }\n}\n\nextension SiteSoftwareType {\n    var label: LocalizedStringResource {\n        switch self {\n        case .lemmy: \"Lemmy\"\n        case .pieFed: \"PieFed\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/SiteSoftwareType+Extensions.swift",
    "content": "//\n//  SiteSoftwareType+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-07-15.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nextension SiteSoftwareType {\n    var minimumSupportedVersion: SiteVersion {\n        switch self {\n        case .lemmy: .init(\"0.19.0\")\n        case .pieFed: .init(\"1.0.0\")\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/UnreadCount+Extensions.swift",
    "content": "//\n//  UnreadCount+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport MlemMiddleware\n\nextension UnreadCount {\n    var badgeLabel: Int? {\n        let total = Settings.get(\\.tab_inbox_badgeIncludedTypes).reduce(0) { $0 + self[$1] }\n        return total <= 0 ? nil : total\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Content Models/VotesModel+Extensions.swift",
    "content": "//\n//  VotesModel+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-17.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nextension VotesModel {\n    var iconName: String {\n        switch myVote {\n        case .upvote: Icons.upvoteSquareFill\n        case .downvote: Icons.downvoteSquareFill\n        case .none: Icons.upvoteSquare\n        }\n    }\n    \n    var iconColor: ThemedColor {\n        switch myVote {\n        case .upvote: .themedUpvote\n        case .downvote: .themedDownvote\n        case .none: .themedSecondary\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Data+Extensions.swift",
    "content": "//\n//  Data+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-10-10.\n//\n\nimport Foundation\n\nextension Data {\n    func writeToTempFile(fileName: String) throws -> URL {\n        let fileUrl = FileManager.default.temporaryDirectory.appending(path: fileName)\n        if FileManager.default.fileExists(atPath: fileUrl.absoluteString) {\n            try FileManager.default.removeItem(at: fileUrl)\n        }\n        try write(to: fileUrl)\n        return fileUrl\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Date+Extensions.swift",
    "content": "//\n//  Date+Extensions.swift\n//  Mlem\n//\n//  Created by Jake Shirley on 6/22/23.\n//\n\nimport SwiftUI\n\nextension Date {\n    /// Forges a localized `String` with inside the computed elapsed time between `self` and another `Date`.\n    /// Uses a `RelativeDateTimeFormatter` and a given units style.\n    ///\n    /// For example, if the `self` is date 04/11/2023 and `date` is 15/05/2025, will return \"1 year ago\" (localized).\n    ///\n    /// - Parameters:\n    ///    - date: The date to compare with `self`, by default `Date.now`\n    ///    - unitsStyle: The style of the string to forge, by default `RelativeDateTimeFormatter.UnitsStyle.full`\n    /// - Returns String: The localized string based.\n    public func getRelativeTime(date: Date = .now, unitsStyle: RelativeDateTimeFormatter.UnitsStyle = .full) -> String {\n        let formatter = RelativeDateTimeFormatter()\n        formatter.unitsStyle = unitsStyle\n        return formatter.localizedString(for: self, relativeTo: date)\n    }\n\n    /// Returns the current `Date` as a shorter version in `String`.\n    /// For example if the date in the 5th of October 2023, returns \"5/10/2023\".\n    /// Uses the current locale to let the `DateFormatter` apply the suitable date format depending to the user needs.\n    public var dateString: String {\n        let dateFormatter = DateFormatter()\n        dateFormatter.dateStyle = .short\n        dateFormatter.timeStyle = .none\n        dateFormatter.locale = Locale.current\n        return dateFormatter.string(from: self)\n    }\n    \n    func getShortRelativeTime(date: Date = .now, unitsStyle: DateComponentsFormatter.UnitsStyle = .abbreviated) -> String {\n        let formatter = DateComponentsFormatter()\n        formatter.unitsStyle = unitsStyle\n        formatter.maximumUnitCount = 1\n        \n        let interval = date.timeIntervalSince(self)\n        if interval < 1 {\n            return String(localized: \"Now\")\n        }\n        \n        let components = Calendar.current.dateComponents(\n            [.year, .month, .day, .hour, .minute, .second],\n            from: self,\n            to: date\n        ).roundingDownToMostSignificantComponent()\n\n        let value = formatter.string(from: components)\n        return value ?? String(localized: \"Unknown\")\n    }\n    \n    var isAnniversaryToday: Bool {\n        let calendar = Calendar.current\n        let date = calendar.dateComponents([.month, .day, .year], from: self)\n        let current = calendar.dateComponents([.month, .day, .year], from: .now)\n        return date.month == current.month && date.day == current.day && date.year != current.year\n    }\n    \n    // https://stackoverflow.com/a/48652058/17629371\n    func messagesRelativeDate() -> String {\n        let dateFormatter = DateFormatter()\n        let calendar = Calendar(identifier: .gregorian)\n        dateFormatter.doesRelativeDateFormatting = true\n\n        if calendar.isDateInToday(self) {\n            dateFormatter.timeStyle = .short\n            dateFormatter.dateStyle = .medium\n        } else if calendar.isDateInYesterday(self) {\n            dateFormatter.timeStyle = .short\n            dateFormatter.dateStyle = .medium\n        } else if calendar.compare(Date(), to: self, toGranularity: .weekOfYear) == .orderedSame {\n            let weekday = calendar.dateComponents([.weekday], from: self).weekday ?? 0\n            return dateFormatter.weekdaySymbols[weekday - 1]\n        } else {\n            dateFormatter.timeStyle = .none\n            dateFormatter.dateStyle = .short\n        }\n\n        return dateFormatter.string(from: self)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/DateComponents+Extensions.swift",
    "content": "//\n//  DateComponents+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-04-22.\n//\n\nimport Foundation\n\nextension DateComponents {\n    // This is used to fix #1988\n    func roundingDownToMostSignificantComponent() -> DateComponents {\n        if let year, year >= 1 { return .init(year: year) }\n        if let month, month >= 1 { return .init(month: month) }\n        if let day, day >= 1 { return .init(day: day) }\n        if let hour, hour >= 1 { return .init(hour: hour) }\n        if let minute, minute >= 1 { return .init(minute: minute) }\n        return self\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/EnvironmentValues+Extensions.swift",
    "content": "//\n//  EnvironmentValues+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 19/09/2024.\n//\n\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nextension EnvironmentValues {\n    @Entry var postContext: Post?\n    @Entry var commentContext: Comment?\n    @Entry var communityContext: Community?\n    @Entry var reportContext: Report?\n    @Entry var feedContext: FeedContext?\n    @Entry var feedLoader: (any FeedLoading)?\n    \n    @Entry var parentFrameWidth: CGFloat = .zero\n    @Entry var isRootView: Bool = false\n    \n    @Entry var scrollProxy: ScrollViewProxy?\n    @Entry var exposeRemovedContent: Bool = false\n\n    @Entry var isContextMenu: Bool = false\n    \n    var appState: AppState {\n        if let appState = self[AppState.self] {\n            return appState\n        } else {\n            assertionFailure()\n            return .main\n        }\n    }\n\n    var hapticManager: HapticManager {\n        if let hapticManager = self[HapticManager.self] {\n            return hapticManager\n        } else {\n            assertionFailure()\n            return .main\n        }\n    }\n\n    var popupModel: PopupAnchorModel? { self[PopupAnchorModel.self] }\n    var toastModel: ToastModel? { self[ToastModel.self] }\n    var navigation: NavigationLayer? { self[NavigationLayer.self] }\n    var commentTreeTracker: CommentTreeTracker? { self[CommentTreeTracker.self] }\n}\n\nstruct RootLayer {\n    let layer: NavigationLayer\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/FederationMode+Extensions.swift",
    "content": "//\n//  FederationMode+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-11-30.\n//  \n\nimport Foundation\nimport Theming\nimport MlemMiddleware\n\nextension FederationMode {\n    var label: LocalizedStringResource {\n        switch self {\n        case .all: \"Yes\"\n        case .local: \"Local Only\"\n        case .disable: \"No\"\n        }\n    }\n    \n    var color: ThemedColor {\n        switch self {\n        case .all: .themedPositive\n        case .local: .themedWarning\n        case .disable: .themedNegative\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/GetContentFilter+Extensions.swift",
    "content": "//\n//  GetContentFilter+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-29.\n//  \n\nimport Foundation\nimport MlemMiddleware\n\nextension GetContentFilter {\n    var label: LocalizedStringResource {\n        switch self {\n        case .saved: \"Saved\"\n        case .upvoted: \"Upvoted\"\n        case .downvoted: \"Downvoted\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/HapticLevel+Extensions.swift",
    "content": "//\n//  HapticTier+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-05-29.\n//\n\nimport Foundation\nimport Haptics\n\nextension HapticTier {\n    var label: LocalizedStringResource {\n        switch self {\n        case .low: \"Low\"\n        case .high: \"High\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/InstanceSummarySoftware+Extensions.swift",
    "content": "//\n//  InstanceSummarySoftware+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-03-27.\n//\n\nimport MlemMiddleware\nimport MlemBackend\n\nextension InstanceSummarySoftware {\n    init(from software: SiteSoftware) {\n        let type: InstanceSummarySoftwareType = switch software.type {\n        case .lemmy: .lemmy\n        case .pieFed: .pieFed\n        }\n        \n        self.init(\n            type: type,\n            version: software.version.description\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Int+Extensions.swift",
    "content": "//\n//  Int+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 13/04/2024.\n//\n\nimport Foundation\n\nextension Int {\n    var abbreviated: String {\n        if self >= 10_000_000 {\n            return \"\\(Int(floor(Double(self) / 1_000_000)))M\"\n        }\n        if self >= 1_000_000 {\n            return \"\\(Double(floor(Double(self) / 100_000) / 10))M\"\n        }\n        if self >= 10000 {\n            return \"\\(Int(floor(Double(self) / 1000)))K\"\n        }\n        if self >= 1000 {\n            return \"\\(Double(floor(Double(self) / 100) / 10))K\"\n        }\n        return String(self)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/ListingType+Extensions.swift",
    "content": "//\n//  ApiListingType+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 29/07/2024.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nextension ListingType {\n    var label: LocalizedStringResource {\n        switch self {\n        case .all: \"All\"\n        case .local: \"Local\"\n        case .subscribed: \"Subscribed\"\n        case .moderated: \"Moderated\"\n        case .popular: \"Popular\"\n        case .suggested: \"Suggested\"\n        }\n    }\n\n    static var guestCases: [ListingType] {\n        [.all, .local, .popular]\n    }\n\n    static var userCases: [ListingType] {\n        [.all, .local, .popular, .suggested, .subscribed]\n    }\n\n    static var moderatorCases: [ListingType] { allCases }\n\n    static func cases(for accountType: AccountType, api: ApiClient) -> [Self] {\n        let cases = switch accountType {\n        case .guest: guestCases\n        case .user: userCases\n        case .moderator, .admin: moderatorCases\n        }\n        return cases.filter { api.supports(.listingType($0), defaultValue: false) }\n    }\n\n    var description: FeedDescription {\n        switch self {\n        case .all: .all\n        case .local: .local\n        case .subscribed: .subscribed\n        case .moderated: .moderated\n        case .popular: .popular\n        case .suggested: .suggested\n        }\n    }\n    \n    var feedContext: FeedContext {\n        switch self {\n        case .all: .all\n        case .local: .local\n        case .subscribed: .subscribed\n        case .moderated: .moderated\n        case .popular: .popular\n        case .suggested: .suggested\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MarkdownConfiguration+Extensions.swift",
    "content": "//\n//  MarkdownConfiguration+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/05/2024.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport Nuke\nimport Rest\nimport SwiftUI\nimport Theming\n\nenum MarkdownConfigurationType {\n    case `default`, defaultBlurred, dimmed, caption, inverted, removedContent\n}\n\nextension MarkdownConfiguration {\n    init(type: MarkdownConfigurationType, palette: Palette) {\n        self = switch type {\n        case .default: .default(palette: palette)\n        case .defaultBlurred: .defaultBlurred(palette: palette)\n        case .dimmed: .dimmed(palette: palette)\n        case .caption: .caption(palette: palette)\n        case .inverted: .inverted(palette: palette)\n        case .removedContent: .removedContent(palette: palette)\n        }\n    }\n    \n    static func `default`(palette: Palette) -> MarkdownConfiguration {\n        let currentPaletteOption = Settings.get(\\.appearance_palette)\n        let enableSyntaxHighlighting = ![.solarized, .monochrome].contains(currentPaletteOption)\n\n        return .init(\n            inlineImageLoader: loadInlineImage,\n            imageBlockView: { imageView($0, shouldBlur: false) },\n            wrapCodeBlockLines: Settings.get(\\.markdown_wrapCodeBlockLines),\n            spoilerLabel: .init(localized: \"Spoiler\"),\n            tableLabel: .init(localized: \"Table\"),\n            censorLabel: .init(localized: \"Censored\"),\n            primaryColor: palette.label.primary,\n            secondaryColor: palette.label.secondary,\n            codeBackgroundColor: palette.groupedBackground.tertiary,\n            censorColor: palette.warning,\n            codeFontScaleFactor: 0.9,\n            enableSyntaxHighlighting: enableSyntaxHighlighting\n        )\n    }\n    \n    static func defaultBlurred(palette: Palette) -> MarkdownConfiguration {\n        var config = Self.default(palette: palette)\n        config.imageBlockView = { imageView($0, shouldBlur: true) }\n        return config\n    }\n    \n    static func dimmed(palette: Palette) -> MarkdownConfiguration {\n        var config = Self.default(palette: palette)\n        \n        // Don't load any images; they will remain as placeholders\n        config.imagePresentationMode = .inline\n        config.inlineImageLoader = { _ in }\n        \n        config.primaryColor = palette.label.secondary\n        config.secondaryColor = palette.label.tertiary\n        \n        return config\n    }\n    \n    static func caption(palette: Palette) -> MarkdownConfiguration {\n        var config = Self.default(palette: palette)\n        config.font = .preferredFont(forTextStyle: .caption1)\n        return config\n    }\n    \n    static func inverted(palette: Palette) -> MarkdownConfiguration {\n        var config = Self.default(palette: palette)\n        config.primaryColor = palette.contrastingLabel\n        config.secondaryColor = palette.contrastingLabel.opacity(0.8)\n        config.spoilerHeaderBackgroundColor = palette.contrastingLabel.opacity(0.1)\n        config.spoilerOutlineColor = palette.contrastingLabel.opacity(0.5)\n        config.codeBackgroundColor = palette.contrastingLabel.opacity(0.1)\n        return config\n    }\n    \n    static func removedContent(palette: Palette) -> MarkdownConfiguration {\n        var config = Self.default(palette: palette)\n        config.primaryColor = palette.negative\n        config.secondaryColor = palette.negative.opacity(0.8)\n        config.spoilerHeaderBackgroundColor = palette.negative.opacity(0.1)\n        config.spoilerOutlineColor = palette.negative.opacity(0.5)\n        config.codeBackgroundColor = palette.negative.opacity(0.1)\n        return config\n    }\n}\n\nprivate func imageView(_ image: MarkdownImage, shouldBlur: Bool) -> AnyView {\n    if image.url.absoluteString == \"https://ko-fi.com/img/githubbutton_sm.svg\" {\n        return AnyView(ShieldsBadgeView(label: \"KoFi\", message: nil, link: image.parentLink))\n    }\n    switch image.url.host() {\n    case \"img.shields.io\":\n        return AnyView(ShieldsBadgeView(shieldsUrl: image.url, link: image.parentLink))\n    case \"fediseer.com\":\n        return AnyView(ShieldsBadgeView(label: \"Fediseer\", message: nil, link: image.parentLink))\n    case \"lemmy-status.org\":\n        return AnyView(ShieldsBadgeView(label: .init(localized: \"Uptime\"), message: nil, link: image.parentLink))\n    default:\n        return AnyView(\n            MediaView.largeImage(url: image.url, shouldBlur: shouldBlur)\n        )\n    }\n}\n\nprivate func loadInlineImage(inlineImage: MarkdownImage) async {\n    guard inlineImage.image == nil else { return }\n    let urlRequest = mlemUrlRequest(url: inlineImage.url)\n    let imageTask = ImagePipeline.shared.imageTask(with: .init(urlRequest: urlRequest))\n    guard let image: UIImage = try? await imageTask.image else { return }\n    let height = inlineImage.fontSize\n    let width = image.size.width * (height / image.size.height)\n    Task { @MainActor in\n        let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: height))\n        let newImage = renderer.image { _ in\n            image.draw(in: CGRect(x: 0, y: 0, width: width, height: height))\n        }\n        inlineImage.image = Image(uiImage: newImage)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/ActorIdentifier+Mock.swift",
    "content": "//\n//  ActorIdentifier+Mock.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-02.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nprivate let hosts = [\n    \"lemm.ee\",\n    \"lemmy.world\",\n    \"sh.itjust.works\",\n    \"sopuli.xyz\",\n    \"programming.dev\",\n    \"lemmy.zip\"\n]\n\nextension ActorIdentifier {\n    static func mockPerson(name: String) -> ActorIdentifier {\n        // Poor man's hash - using `hashValue` directly gives a different value each time the program is executed\n        let hashValue = name.unicodeScalars.reduce(0) { $0 + Int($1.value) }\n        \n        var generator = SeededRandomNumberGenerator(seed: hashValue)\n        let value = Int.random(in: 0 ..< hosts.count, using: &generator)\n        let host = hosts[value]\n        return .init(url: URL(string: \"https://\\(host)/u/\\(name)\")!)!\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/Comment+Mock.swift",
    "content": "//\n//  Comment+Mock.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-15.\n//\n\n// TODO: updated mocks\n// import Foundation\n// import MlemMiddleware\n//\n// extension Comment1 {\n//    static func mock(\n//        _ type: CommentMockType,\n//        api: MockApiClient = .mock\n//    ) -> Comment1 {\n//        .mock(\n//            api: api,\n//            id: type.id,\n//            content: type.content,\n//            removed: false,\n//            created: type.created,\n//            updated: nil,\n//            deleted: false,\n//            creatorId: type.creator.id,\n//            postId: type.post.id,\n//            parentCommentIds: type.parentComments.map(\\.id),\n//            distinguished: false,\n//            languageId: 0\n//        )\n//    }\n// }\n//\n// extension Comment2 {\n//    static func mock(\n//        _ type: CommentMockType,\n//        api: MockApiClient = .mock\n//    ) -> Comment2 {\n//        .mock(\n//            api: api,\n//            comment1: .mock(type, api: api),\n//            creator: .mock(type.creator, api: api),\n//            post: .mock(type.post, api: api),\n//            community: .mock(type.post.community, api: api),\n//            votes: type.votes,\n//            saved: false,\n//            creatorIsModerator: false,\n//            creatorIsAdmin: false,\n//            bannedFromCommunity: false,\n//            commentCount: type.commentCount\n//        )\n//    }\n// }\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/CommentMockType.swift",
    "content": "//\n//  CommentMockType.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-17.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nenum CommentMockType {\n    case generic\n    \n    var id: Int {\n        switch self {\n        case .generic: 0\n        }\n    }\n    \n    var content: String {\n        switch self {\n        // swiftlint:disable:next line_length\n        case .generic: \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\"\n        }\n    }\n    \n    var created: Date {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        let lowerBound = 60 * 60 * 1 // 1h\n        let upperBound = 60 * 60 * 24 // 24h\n        let timeInterval = TimeInterval(Int.random(in: lowerBound ... upperBound, using: &generator))\n        return .now.addingTimeInterval(-timeInterval)\n    }\n    \n    var votes: VotesModel {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        let score = Int.random(in: 100 ... 1000, using: &generator)\n        return .init(upvotes: Int(Double(score) * 0.8), downvotes: Int(Double(score) * 0.2), myVote: .none)\n    }\n    \n    var post: PostMockType {\n        switch self {\n        case .generic: .generic\n        }\n    }\n    \n    var creator: PersonMockType {\n        switch self {\n        case .generic: .generic\n        }\n    }\n    \n    var parentComments: [CommentMockType] {\n        switch self {\n        case .generic: []\n        }\n    }\n    \n    var commentCount: Int {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        return Int.random(in: 0 ... 50, using: &generator)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/Community+Mock.swift",
    "content": "//\n//  Community+Mock.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-03.\n//\n\nimport Foundation\nimport MlemMiddleware\n\n// TODO: updated mocks\n// extension Community1 {\n//    static func mock(\n//        _ type: CommunityMockType,\n//        api: MockApiClient = .mock\n//    ) -> Community1 {\n//        .mock(\n//            api: api,\n//            actorId: type.actorId,\n//            id: type.id,\n//            name: type.name,\n//            created: type.created,\n//            instanceId: 0,\n//            updated: nil,\n//            displayName: type.displayName,\n//            description: type.description,\n//            removed: false,\n//            deleted: false,\n//            nsfw: false,\n//            avatar: type.avatar,\n//            banner: type.banner,\n//            hidden: false,\n//            onlyModeratorsCanPost: false,\n//            blocked: false\n//        )\n//    }\n// }\n//\n// extension Community2 {\n//    static func mock(\n//        _ type: CommunityMockType,\n//        api: MockApiClient = .mock\n//    ) -> Community2 {\n//        .mock(\n//            community1: .mock(type, api: api),\n//            subscriberCount: type.subscriberCount,\n//            localSubscriberCount: type.localSubscriberCount,\n//            subscribed: false,\n//            subscriptionPending: false,\n//            postCount: type.postCount,\n//            commentCount: type.commentCount,\n//            activeUserCount: .init(sixMonths: 0, month: 0, week: 0, day: 0),\n//            bannedFromCommunity: nil\n//        )\n//    }\n// }\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/CommunityMockType+Realistic.swift",
    "content": "//\n//  CommunityMockType+Realistic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-04.\n//\n\nimport Foundation\n\nextension CommunityMockType {\n    // These values are localized for use in marketing material (e.g. preview images on the App Store)\n    enum Realistic: CaseIterable, Identifiable {\n        case news\n        case pics\n        case meIrl\n        case technology\n        case nature\n        case showerThoughts\n        \n        var id: Int {\n            switch self {\n            case .news: 0\n            case .pics: 1\n            case .meIrl: 2\n            case .technology: 3\n            case .nature: 4\n            case .showerThoughts: 5\n            }\n        }\n        \n        var name: String {\n            switch self {\n            case .news: .init(\n                    localized: \"community.1.name\",\n                    defaultValue: \"news\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .pics: .init(\n                    localized: \"community.2.name\",\n                    defaultValue: \"pics\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .meIrl: .init(\n                    localized: \"community.3.name\",\n                    defaultValue: \"me_irl\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .technology: .init(\n                    localized: \"community.4.name\",\n                    defaultValue: \"technology\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .nature: .init(\n                    localized: \"community.5.name\",\n                    defaultValue: \"nature\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .showerThoughts: .init(\n                    localized: \"community.6.name\",\n                    defaultValue: \"showerthoughts\",\n                    table: \"PreviewLocalizable\"\n                )\n            }\n        }\n        \n        var displayName: String {\n            switch self {\n            case .news: .init(\n                    localized: \"community.1.displayName\",\n                    defaultValue: \"World News\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .pics: .init(\n                    localized: \"community.2.displayName\",\n                    defaultValue: \"Pics\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .meIrl: .init(\n                    localized: \"community.3.displayName\",\n                    defaultValue: \"me_irl\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .technology: .init(\n                    localized: \"community.4.displayName\",\n                    defaultValue: \"Technology\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .nature: .init(\n                    localized: \"community.5.displayName\",\n                    defaultValue: \"Nature\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .showerThoughts: .init(\n                    localized: \"community.6.displayName\",\n                    defaultValue: \"Nature\",\n                    table: \"PreviewLocalizable\"\n                )\n            }\n        }\n        \n        var description: String? {\n            switch self {\n            case .news: nil\n            case .pics: nil\n            case .meIrl: nil\n            case .technology: nil\n            case .nature: nil\n            case .showerThoughts: nil\n            }\n        }\n        \n        var avatar: URL? {\n            switch self {\n            case .news: .init(string: \"mlempreview://image/pfp.news\")\n            case .pics: .init(string: \"mlempreview://image/pfp.balloon\")\n            case .meIrl: .init(string: \"mlempreview://image/pfp.person\")\n            case .technology: .init(string: \"mlempreview://image/pfp.circuit\")\n            case .nature: .init(string: \"mlempreview://image/pfp.lakeside\")\n            case .showerThoughts: .init(string: \"mlempreview://image/pfp.shower\")\n            }\n        }\n        \n        var banner: URL? {\n            switch self {\n            case .news: nil\n            case .pics: nil\n            case .meIrl: nil\n            case .technology: nil\n            case .nature: nil\n            case .showerThoughts: .init(string: \"mlempreview://image.droplets\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/CommunityMockType.swift",
    "content": "//\n//  CommunityMockType.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-03.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nenum CommunityMockType {\n    case realistic(Realistic)\n    case generic\n    \n    var id: Int {\n        switch self {\n        case let .realistic(value): 100 + value.id\n        case .generic: 0\n        }\n    }\n    \n    var actorId: ActorIdentifier {\n        switch self {\n        case let .realistic(value):\n            .mockPerson(name: value.name)\n        case .generic:\n            .init(url: URL(string: \"https://example.com/c/\\(name)\")!)!\n        }\n    }\n    \n    var name: String {\n        switch self {\n        case let .realistic(value): value.name\n        case .generic: \"community\"\n        }\n    }\n    \n    var displayName: String {\n        switch self {\n        case let .realistic(value): value.displayName\n        case .generic: \"Community\"\n        }\n    }\n    \n    var description: String? {\n        switch self {\n        case let .realistic(value): value.description\n        case .generic: \"ABC\"\n        }\n    }\n    \n    var avatar: URL? {\n        switch self {\n        case let .realistic(value): value.avatar\n        case .generic: nil\n        }\n    }\n    \n    var banner: URL? {\n        switch self {\n        case let .realistic(value): value.banner\n        case .generic: nil\n        }\n    }\n    \n    var created: Date {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        let lowerBound = 60 * 60 * 24 * 30 * 3 // 3mo\n        let upperBound = 60 * 60 * 24 * 365 * 2 // 2y\n        let timeInterval = TimeInterval(Int.random(in: lowerBound ... upperBound, using: &generator))\n        return .now.addingTimeInterval(-timeInterval)\n    }\n    \n    var subscriberCount: Int {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        return Int.random(in: 500 ... 20000, using: &generator)\n    }\n\n    var localSubscriberCount: Int {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        return Int.random(in: 100 ... 1000, using: &generator)\n    }\n\n    var postCount: Int {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        return Int.random(in: 2000 ... 10000, using: &generator)\n    }\n    \n    var commentCount: Int {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        return Int.random(in: 5000 ... 25000, using: &generator)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/MockApiClient+Realistic.swift",
    "content": "//\n//  MockApiClient+Realistic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-23.\n//\n\nimport Foundation\nimport MlemMiddleware\n\n// TODO: updated mocks\n// extension MockApiClient {\n//    static let realistic: MockApiClient = {\n//        let client = MockApiClient()\n//        client.setPosts(PostMockType.Realistic.allCases.map { Post2.mock(.realistic($0), api: client) })\n//        client.setCommunities(CommunityMockType.Realistic.allCases.map { Community2.mock(.realistic($0), api: client) })\n//        client.setPeople(PersonMockType.Realistic.allCases.map { Person2.mock(.realistic($0), api: client) })\n//        return client\n//    }()\n// }\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/Person+Mock.swift",
    "content": "//\n//  Person1+Mock.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-02.\n//\n\nimport Foundation\nimport MlemMiddleware\n\n// TODO: updated mocks\n// extension Person1 {\n//    static func mock(\n//        _ type: PersonMockType,\n//        api: MockApiClient = .mock\n//    ) -> Person1 {\n//        .mock(\n//            api: api,\n//            actorId: type.actorId,\n//            id: type.id,\n//            name: type.name,\n//            created: type.created,\n//            instanceId: 0,\n//            updated: nil,\n//            displayName: type.displayName,\n//            description: type.description,\n//            matrixUserId: type.matrixUserId,\n//            avatar: type.avatar,\n//            banner: type.banner,\n//            deleted: false,\n//            isBot: type.isBot,\n//            instanceBan: .notBanned,\n//            blocked: false\n//        )\n//    }\n// }\n//\n// extension Person2 {\n//    static func mock(\n//        _ type: PersonMockType,\n//        api: MockApiClient = .mock,\n//        isAdmin: Bool = false\n//    ) -> Person2 {\n//        .mock(\n//            person1: .mock(type, api: api),\n//            postCount: type.postCount,\n//            commentCount: type.commentCount,\n//            isAdmin: isAdmin\n//        )\n//    }\n// }\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/PersonMockType+Realistic.swift",
    "content": "//\n//  PersonMockType+Realistic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-02.\n//\n\nimport Foundation\n\nextension PersonMockType {\n    // These values are localized for use in marketing material (e.g. preview images on the App Store)\n    enum Realistic: CaseIterable, Identifiable {\n        case flowerTail\n        case commanderGoose\n        case billyDaFish\n        case grt38\n        case anteSocial45\n        \n        var id: Int {\n            switch self {\n            case .flowerTail: 0\n            case .commanderGoose: 1\n            case .billyDaFish: 2\n            case .grt38: 3\n            case .anteSocial45: 4\n            }\n        }\n        \n        var name: String {\n            switch self {\n            case .flowerTail: .init(\n                    localized: \"person.1.name\",\n                    defaultValue: \"flowertail\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .commanderGoose: .init(\n                    localized: \"person.2.name\",\n                    defaultValue: \"CommanderGoose\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .billyDaFish: .init(\n                    localized: \"person.3.name\",\n                    defaultValue: \"BillyDAFISH\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .grt38: .init(\n                    localized: \"person.4.name\",\n                    defaultValue: \"Grt38\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .anteSocial45: .init(\n                    localized: \"person.5.name\",\n                    defaultValue: \"ante_social_58\",\n                    table: \"PreviewLocalizable\"\n                )\n            }\n        }\n        \n        var displayName: String {\n            switch self {\n            case .flowerTail: .init(\n                    localized: \"person.1.displayName\",\n                    defaultValue: \"Flowertail\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .commanderGoose: .init(\n                    localized: \"person.2.displayName\",\n                    defaultValue: \"Commander Goose\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .billyDaFish: .init(\n                    localized: \"person.3.displayName\",\n                    defaultValue: \"BillyDAFISH\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .grt38: .init(\n                    localized: \"person.4.displayName\",\n                    defaultValue: \"Grt38\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .anteSocial45: .init(\n                    localized: \"person.5.displayName\",\n                    defaultValue: \"AnteSocial\",\n                    table: \"PreviewLocalizable\"\n                )\n            }\n        }\n        \n        var description: String? {\n            switch self {\n            case .flowerTail: nil\n            case .commanderGoose: .init(\n                    localized: \"person.2.description\",\n                    defaultValue: \"HONK\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .billyDaFish: nil\n            case .grt38: nil\n            case .anteSocial45: nil\n            }\n        }\n        \n        var avatar: URL? {\n            switch self {\n            case .flowerTail: .init(string: \"mlempreview://image/pfp.flowers\")\n            case .commanderGoose: .init(string: \"mlempreview://image/pfp.goose\")\n            case .billyDaFish: .init(string: \"mlempreview://image/pfp.fish\")\n            case .grt38: .init(string: \"mlempreview://image/pfp.firework\")\n            case .anteSocial45: nil\n            }\n        }\n        \n        var banner: URL? {\n            switch self {\n            case .flowerTail: nil\n            case .commanderGoose: nil\n            case .billyDaFish: nil\n            case .grt38: nil\n            case .anteSocial45: nil\n            }\n        }\n        \n        var matrixUserId: String? {\n            switch self {\n            case .flowerTail: nil\n            case .commanderGoose: nil\n            case .billyDaFish: nil\n            case .grt38: nil\n            case .anteSocial45: nil\n            }\n        }\n        \n        var isBot: Bool {\n            switch self {\n            default: false\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/PersonMockType.swift",
    "content": "//\n//  PersonMockType.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-02.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nenum PersonMockType: Identifiable {\n    case realistic(Realistic)\n    case generic\n    \n    var id: Int {\n        switch self {\n        case let .realistic(value): 100 + value.id\n        case .generic: 0\n        }\n    }\n    \n    var actorId: ActorIdentifier {\n        switch self {\n        case let .realistic(value):\n            .mockPerson(name: value.name)\n        case .generic:\n            .init(url: URL(string: \"https://example.com/u/\\(name)\")!)!\n        }\n    }\n    \n    var name: String {\n        switch self {\n        case let .realistic(value): value.name\n        case .generic: \"user\"\n        }\n    }\n    \n    var displayName: String {\n        switch self {\n        case let .realistic(value): value.displayName\n        case .generic: \"User\"\n        }\n    }\n    \n    var description: String? {\n        switch self {\n        case let .realistic(value): value.description\n        case .generic: \"ABC\"\n        }\n    }\n    \n    var avatar: URL? {\n        switch self {\n        case let .realistic(value): value.avatar\n        case .generic: nil\n        }\n    }\n    \n    var banner: URL? {\n        switch self {\n        case let .realistic(value): value.banner\n        case .generic: nil\n        }\n    }\n    \n    var created: Date {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        let lowerBound = 60 * 5 // 5h\n        let upperBound = 60 * 60 * 24 * 365 * 2 // 2y\n        let timeInterval = TimeInterval(Int.random(in: lowerBound ... upperBound, using: &generator))\n        return .now.addingTimeInterval(-timeInterval)\n    }\n    \n    var matrixUserId: String? {\n        switch self {\n        case let .realistic(value): value.matrixUserId\n        case .generic: nil\n        }\n    }\n    \n    var isBot: Bool {\n        switch self {\n        case let .realistic(value): value.isBot\n        case .generic: false\n        }\n    }\n    \n    var postCount: Int {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        return Int.random(in: 0 ... 100, using: &generator)\n    }\n    \n    var commentCount: Int {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        return Int.random(in: 0 ... 700, using: &generator)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/Post+Mock.swift",
    "content": "//\n//  Post1+Mock.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-02.\n//\n\nimport Foundation\nimport MlemMiddleware\n\n// TODO: updated mocks\n// extension Post1 {\n//    static func mock(\n//        _ type: PostMockType,\n//        api: MockApiClient = .mock,\n//        deleted: Bool = false,\n//        pinnedCommunity: Bool = false,\n//        pinnedInstance: Bool = false,\n//        locked: Bool = false,\n//        nsfw: Bool = false,\n//        removed: Bool = false\n//    ) -> Post1 {\n//        .mock(\n//            api: api,\n//            id: type.id,\n//            creatorId: 0,\n//            communityId: 0,\n//            created: type.created,\n//            title: type.title,\n//            content: type.content,\n//            linkUrl: type.linkUrl,\n//            deleted: deleted,\n//            embed: nil,\n//            pinnedCommunity: pinnedCommunity,\n//            pinnedInstance: pinnedInstance,\n//            locked: locked,\n//            nsfw: nsfw,\n//            removed: removed,\n//            thumbnailUrl: nil,\n//            updated: nil,\n//            languageId: 0,\n//            altText: nil\n//        )\n//    }\n// }\n//\n// extension Post2 {\n//    static func mock(\n//        _ type: PostMockType,\n//        api: MockApiClient = .mock\n//    ) -> Post2 {\n//        .mock(\n//            api: api,\n//            post1: .mock(type, api: api),\n//            creator: .mock(type.creator, api: api),\n//            community: .mock(type.community, api: api),\n//            votes: type.votes,\n//            creatorIsModerator: false,\n//            creatorIsAdmin: false,\n//            creatorBannedFromCommunity: false,\n//            commentCount: type.commentCount,\n//            unreadCommentCount: 0,\n//            saved: false,\n//            read: false,\n//            hidden: false\n//        )\n//    }\n// }\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/PostMockType+Realistic.swift",
    "content": "//\n//  PostMockType+Realistic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-23.\n//\n\nimport Foundation\n\nextension PostMockType {\n    enum Realistic: CaseIterable, Identifiable {\n        case yorkshireDales\n        case meguroRiver\n        case showerThoughtPizza\n        \n        var id: Int {\n            switch self {\n            case .yorkshireDales: 0\n            case .meguroRiver: 1\n            case .showerThoughtPizza: 2\n            }\n        }\n        \n        var title: String {\n            switch self {\n            case .yorkshireDales: .init(\n                    localized: \"post.1.title\",\n                    defaultValue: \"The Yorkshire Dales, England\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .meguroRiver: .init(\n                    localized: \"post.2.title\",\n                    defaultValue: \"Meguro River, Matsuno, Japan\",\n                    table: \"PreviewLocalizable\"\n                )\n            case .showerThoughtPizza: .init(\n                    localized: \"post.3.title\",\n                    // swiftlint:disable:next line_length\n                    defaultValue: \"During a nuclear explosion, there is a certain distance of the radius where all the frozen supermarket pizzas are cooked to perfection.\",\n                    table: \"PreviewLocalizable\"\n                )\n            }\n        }\n        \n        var content: String? {\n            switch self {\n            case .yorkshireDales: nil\n            case .meguroRiver: nil\n            case .showerThoughtPizza: nil\n            }\n        }\n        \n        var linkUrl: URL? {\n            switch self {\n            case .yorkshireDales: .init(string: \"mlempreview://image/image.yorkshire_dales\")\n            case .meguroRiver: .init(string: \"mlempreview://image/image.meguro_river\")\n            case .showerThoughtPizza: nil\n            }\n        }\n        \n        var creator: PersonMockType.Realistic {\n            switch self {\n            case .yorkshireDales: .commanderGoose\n            case .meguroRiver: .anteSocial45\n            case .showerThoughtPizza: .billyDaFish\n            }\n        }\n        \n        var community: CommunityMockType.Realistic {\n            switch self {\n            case .yorkshireDales: .nature\n            case .meguroRiver: .pics\n            case .showerThoughtPizza: .showerThoughts\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/MlemMiddleware Mock/PostMockType.swift",
    "content": "//\n//  PostMockType.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-04.\n//\n\nimport Foundation\nimport MlemMiddleware\n\n// swiftlint:disable line_length\nenum PostMockType {\n    case generic\n    case realistic(Realistic)\n    \n    var id: Int {\n        switch self {\n        case .generic: 0\n        case let .realistic(value): 100 + value.id\n        }\n    }\n    \n    var title: String {\n        switch self {\n        case .generic:\n            \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\"\n        case let .realistic(value): value.title\n        }\n    }\n    \n    var content: String? {\n        switch self {\n        case .generic:\n            \"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\"\n        case let .realistic(value): value.content\n        }\n    }\n    \n    var created: Date {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        let lowerBound = 60 * 60 * 1 // 1h\n        let upperBound = 60 * 60 * 24 // 24h\n        let timeInterval = TimeInterval(Int.random(in: lowerBound ... upperBound, using: &generator))\n        return .now.addingTimeInterval(-timeInterval)\n    }\n    \n    var votes: VotesModel {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        let score = Int.random(in: 100 ... 1000, using: &generator)\n        return .init(upvotes: Int(Double(score) * 0.8), downvotes: Int(Double(score) * 0.2), myVote: .none)\n    }\n    \n    var linkUrl: URL? {\n        switch self {\n        case .generic: nil\n        case let .realistic(value): value.linkUrl\n        }\n    }\n    \n    var creator: PersonMockType {\n        switch self {\n        case .generic: .generic\n        case let .realistic(value): .realistic(value.creator)\n        }\n    }\n    \n    var community: CommunityMockType {\n        switch self {\n        case .generic: .generic\n        case let .realistic(value): .realistic(value.community)\n        }\n    }\n    \n    var commentCount: Int {\n        var generator = SeededRandomNumberGenerator(seed: id)\n        return Int.random(in: 0 ... 50, using: &generator)\n    }\n}\n\n// swiftlint:enable line_length\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/PersonContentFeedLoader+Extensions.swift",
    "content": "//\n//  SingleSourceMixedFeedLoader+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-29.\n//  \n\nimport MlemMiddleware\n\nextension SingleSourceMixedFeedLoader {\n    func itemsForType(_ type: PersonContentType) -> [PersonContent] {\n        switch type {\n        case .all: items\n        case .posts: posts\n        case .comments: comments\n        }\n    }\n    \n    func loadingStateForType(_ type: PersonContentType) -> FeedLoadingState {\n        switch type {\n        case .all: loadingState\n        case .posts: postLoadingState\n        case .comments: commentLoadingState\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/PostSortType+Extensions.swift",
    "content": "//\n//  PostSortType+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-28.\n//\n\nimport Foundation\nimport Icons\nimport MlemMiddleware\n\nextension PostSortType {\n    func label(timeRangeFormat: SortTimeRange.FormatStyle = .timescaleFull) -> String {\n        switch self {\n        case .active:\n            .init(localized: \"Active\")\n        case .hot:\n            .init(localized: \"Hot\")\n        case .new:\n            .init(localized: \"New\")\n        case .old:\n            .init(localized: \"Old\")\n        case let .top(timeRange):\n            timeRange.label(name: \"Top\", prefix: \"Top:\", format: timeRangeFormat)\n        case .mostComments:\n            .init(localized: \"Most Comments\")\n        case .newComments:\n            .init(localized: \"New Comments\")\n        case .controversial:\n            .init(localized: \"Controversial\")\n        case .scaled:\n            .init(localized: \"Scaled\")\n        }\n    }\n\n    var icon: Icon {\n        switch self {\n        case .active: .lemmy.activeSort\n        case .hot: .lemmy.hotSort\n        case .new: .lemmy.newSort\n        case .old: .lemmy.oldSort\n        case .mostComments: .lemmy.mostCommentsSort\n        case .newComments: .lemmy.newCommentsSort\n        case .controversial: .lemmy.controversialSort\n        case .scaled: .lemmy.scaledSort\n        case .top: .lemmy.topSort\n        }\n    }\n    \n    var explanation: LocalizedStringResource? {\n        switch self {\n        case .hot: \"Ranks posts based on the post score and creation time.\"\n        case .scaled: \"Similar to Hot, but ranks posts from smaller communities higher.\"\n        case .active: \"Ranks posts based on the post score and the time since the last comment was created.\"\n        default: nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/PostType+Extensions.swift",
    "content": "//\n//  PostType+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-03.\n//\n\nimport MlemMiddleware\n\nextension PostType {\n    var lineLimit: Int {\n        switch self {\n        case .text, .titleOnly: 4\n        case .media, .embedded, .link, .poll: 2\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/PrefetchingConfiguration+Extensions.swift",
    "content": "//\n//  PrefetchingConfiguration+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 10/08/2024.\n//\n\nimport MlemMiddleware\nimport Nuke\n\nextension PrefetchingConfiguration {\n    static func forPostSize(_ postSize: PostSize) -> Self {\n        .init(\n            prefetcher: .init(pipeline: .shared, destination: .memoryCache, maxConcurrentRequestCount: 40),\n            imageSize: .unlimited,\n            fetchFavicons: Settings.get(\\.privacy_showFavicons),\n            embedLoops: Settings.get(\\.links_embedLoops),\n            avatarSize: postSize.avatarSize\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/QuickSwipeAction+Actions.swift",
    "content": "//\n//  QuickSwipeAction+Actions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-11-18.\n//\n\nimport Actions\nimport Icons\nimport QuickSwipes\nimport SwiftUI\n\nprivate extension QuickSwipeAction {\n    init?(label: ActionLabel, callback: @escaping () -> Void) {\n        if label.visibility == .hidden { return nil }\n        self.init(\n            icon: label.icon,\n            color: label.color,\n            enabled: label.visibility == .enabled,\n            confirmationPrompt: nil,\n            callback: callback\n        )\n    }\n}\n\nprivate struct QuickSwipesActionsViewModifier: ViewModifier {\n    @Environment(\\.self) var environment\n\n    let leadingActions: [any Actions.Action]\n    let trailingActions: [any Actions.Action]\n\n    func body(content: Content) -> some View {\n        content\n            .quickSwipes(config)\n    }\n\n    var config: SwipeConfiguration {\n        .init(\n            leadingActions: leadingActions.compactMap(self.createAction),\n            trailingActions: trailingActions.compactMap(self.createAction)\n        )\n    }\n\n    func createAction(_ action: any Actions.Action) -> QuickSwipeAction? {\n        .init(\n            label: action.createLabel(environment: environment),\n            callback: { action.execute(environment: environment) }\n        )\n    }\n}\n\nextension View {\n    @ViewBuilder\n    func quickSwipes(leading: [any Actions.Action], trailing: [any Actions.Action]) -> some View {\n        modifier(QuickSwipesActionsViewModifier(\n            leadingActions: leading,\n            trailingActions: trailing\n        ))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/QuickSwipeAction+Extensions.swift",
    "content": "//\n//  QuickSwipeAction+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-22.\n//\n\nimport Icons\nimport QuickSwipes\n\nextension QuickSwipeAction {\n    init?(from action: any Action) {\n        switch action {\n        case let action as BasicAction:\n            self.init(action: action)\n        case let group as ActionGroup:\n            self.init(group: group)\n        default:\n            assertionFailure()\n            return nil\n        }\n    }\n    \n    private init(action: BasicAction) {\n        self.init(\n            icon: .init(from: action.appearance),\n            color: action.appearance.color,\n            enabled: action.callback != nil,\n            confirmationPrompt: action.confirmationPrompt,\n            callback: action.callback ?? {}\n        )\n    }\n    \n    private init(group: ActionGroup) {\n        self.init(\n            icon: .init(from: group.appearance),\n            color: group.appearance.color,\n            enabled: true,\n            alertTitle: group.prompt ?? \"\",\n            choices: group.children.compactMap(QuickSwipeChoice.init)\n        )\n    }\n}\n\nextension QuickSwipeChoice {\n    init?(from action: any Action) {\n        switch action {\n        case let action as BasicAction:\n            self.init(\n                label: action.appearance.label,\n                destructive: action.appearance.isDestructive,\n                callback: action.callback ?? {}\n            )\n        default:\n            assertionFailure()\n            return nil\n        }\n    }\n}\n\n// Temporary shim. Eventually the action system will use Icon rather than String and this can be removed\nprivate extension Icon {\n    init(from appearance: ActionAppearance) {\n        self = .custom { variant in\n            switch variant {\n            case .active:\n                return appearance.swipeIcon2\n            case .inactive:\n                return appearance.swipeIcon1\n            default:\n                assertionFailure()\n                return appearance.swipeIcon1\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/RegistrationMode+Extensions.swift",
    "content": "//\n//  ApiRegistrationMode+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/08/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nextension RegistrationMode {\n    var label: LocalizedStringResource {\n        switch self {\n        case .closed: \"Closed\"\n        case .requiresApplication: \"Requires Application\"\n        case .open: \"Open\"\n        }\n    }\n    \n    var color: ThemedColor {\n        switch self {\n        case .closed: .themedNegative\n        case .requiresApplication: .themedCaution\n        case .open: .themedPositive\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/SearchSortType+Extensions.swift",
    "content": "//\n//  SearchSortType+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-01.\n//\n\nimport Foundation\nimport Icons\nimport MlemMiddleware\n\nextension SearchSortType {\n    func label(timeRangeFormat: SortTimeRange.FormatStyle = .timescaleFull) -> String {\n        switch self {\n        case .new:\n            .init(localized: \"New\")\n        case .old:\n            .init(localized: \"Old\")\n        case let .top(timeRange):\n            timeRange.label(name: \"Top\", prefix: \"Top:\", format: timeRangeFormat)\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .new: .lemmy.newSort\n        case .old: .lemmy.oldSort\n        case .top: .lemmy.topSort\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Set+Extensions.swift",
    "content": "//\n//  Set+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-17.\n//\n\nimport Foundation\n\n// https://stackoverflow.com/a/65598711/17629371\nextension Set: @retroactive RawRepresentable where Element: Codable {\n    public init?(rawValue: String) {\n        guard let data = rawValue.data(using: .utf8),\n              let result = try? JSONDecoder().decode(Set<Element>.self, from: data)\n        else {\n            return nil\n        }\n        self = result\n    }\n\n    public var rawValue: String {\n        guard let data = try? JSONEncoder().encode(self),\n              let result = String(data: data, encoding: .utf8)\n        else {\n            return \"[]\"\n        }\n        return result\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/SortTimeRange+Extensions.swift",
    "content": "//\n//  SortTimeRange+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-01.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nextension SortTimeRange {\n    enum FormatStyle {\n        case topOnly\n        case timescaleAbbreviated\n        case timescaleFull\n        case topAndTimescale\n    }\n    \n    func label(name: LocalizedStringResource, prefix: LocalizedStringResource, format: FormatStyle) -> String {\n        switch format {\n        case .topOnly:\n            String(localized: name)\n        case .topAndTimescale:\n            \"\\(String(localized: prefix)) \\(label(abbreviateUnits: false))\"\n        case .timescaleAbbreviated:\n            label(abbreviateUnits: true)\n        case .timescaleFull:\n            label(abbreviateUnits: false)\n        }\n    }\n    \n    private func label(abbreviateUnits: Bool) -> String {\n        switch self {\n        case let .limited(timeInterval):\n            var seconds = Int(timeInterval)\n            \n            let dateComponents: DateComponents\n            \n            // Check if time range is exact number of weeks\n            if seconds % (3600 * 24 * 7) == 0 {\n                dateComponents = .init(weekOfMonth: seconds / (3600 * 24 * 7))\n            } else {\n                // Convert a year to exactly 365 days\n                let years = seconds / (3600 * 24 * 365)\n                seconds %= (3600 * 24 * 365)\n                // Convert a month to exactly 30 days\n                let months = seconds / (3600 * 24 * 30)\n                seconds %= (3600 * 24 * 30)\n                dateComponents = .init(year: years, month: months, second: seconds)\n            }\n            \n            if abbreviateUnits {\n                return formatter(unitsStyle: .abbreviated).string(for: dateComponents) ?? \"\"\n            } else {\n                return formatter(unitsStyle: .full)\n                    .string(for: dateComponents)?\n                    .capitalized ?? \"\"\n            }\n        case .allTime:\n            return abbreviateUnits ? .init(localized: \"All\") : .init(localized: \"All Time\")\n        }\n    }\n    \n    private func formatter(unitsStyle: DateComponentsFormatter.UnitsStyle) -> DateComponentsFormatter {\n        let formatter = DateComponentsFormatter()\n        formatter.unitsStyle = unitsStyle\n        formatter.allowedUnits = [.year, .month, .weekOfMonth, .day, .hour, .minute, .second]\n        return formatter\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/String+Extensions.swift",
    "content": "//\n//  String+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-09-27.\n//\n\nextension String {\n    var isMovieExtension: Bool { [\"mp4\", \"m4v\", \"mov\"].contains(self) }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/String+extension.swift",
    "content": "//\n// Software Name: Mlem\n// SPDX-FileCopyrightText: Copyright (c) Mlem Group\n// SPDX-License-Identifier: GPL-3.0\n//\n// This software is distributed under the GNU General Public License v3.0 license,\n// the text of which is available at https://www.gnu.org/licenses/gpl-3.0-standalone.html\n// or see the \"LICENSE\" file for more details.\n//\n\nimport Foundation\n\nextension String {\n    /// Returns the localized result string using `self` as key.\n    /// - Returns String: The conversion of `self` as `NSLocalizedString`\n    func localized() -> String {\n        let prefferedLocalization = Bundle.preferredLocalization\n\n        guard let path = Bundle.main.path(forResource: prefferedLocalization, ofType: \"lproj\") else {\n            return NSLocalizedString(self, bundle: Bundle.main, comment: \"\")\n        }\n\n        guard let languageBundle = Bundle(path: path) else {\n            return NSLocalizedString(self, bundle: Bundle.main, comment: \"\")\n        }\n\n        return NSLocalizedString(self, tableName: nil, bundle: languageBundle, value: \"\", comment: \"\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/SwipeConfiguration+Extensions.swift",
    "content": "//\n//  SwipeConfiguration+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-22.\n//\n\nimport Foundation\nimport QuickSwipes\n\nextension SwipeConfiguration {\n    // Prevents ambiguous init declaration\n    init() {\n        self.init(leadingActions: [QuickSwipeAction](), trailingActions: [QuickSwipeAction]())\n    }\n    \n    init(\n        leadingActions: [any Action] = [],\n        trailingActions: [any Action] = []\n    ) {\n        self.init(\n            leadingActions: leadingActions.compactMap(QuickSwipeAction.init),\n            trailingActions: trailingActions.compactMap(QuickSwipeAction.init)\n        )\n    }\n    \n    init(\n        @ActionBuilder leadingActions: () -> [any Action] = { [] },\n        @ActionBuilder trailingActions: () -> [any Action] = { [] }\n    ) {\n        self.init(\n            leadingActions: leadingActions().compactMap(QuickSwipeAction.init),\n            trailingActions: trailingActions().compactMap(QuickSwipeAction.init)\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/UIApplication+Extensions.swift",
    "content": "//\n//  UIApplication+FirstKeyWindow.swift\n//  Mlem\n//\n//  Created by David Bureš on 18.05.2023.\n//\n\nimport Foundation\nimport UIKit\n\nextension UIApplication {\n    var firstKeyWindow: UIWindow? {\n        connectedScenes\n            .compactMap { $0 as? UIWindowScene }\n            .filter { $0.activationState == .foregroundActive }\n            .first?\n            .keyWindow\n    }\n}\n\nextension UIApplication {\n    var topMostViewController: UIViewController? {\n        UIApplication.shared.firstKeyWindow?.rootViewController?.topMostViewController()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/UIDevice+Extensions.swift",
    "content": "//\n//  UIDevice+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 13/06/2024.\n//\n\nimport UIKit\n\nextension UIDevice {\n    static var isPad: Bool {\n        UIDevice.current.userInterfaceIdiom == .pad\n    }\n    \n    static var isPhone: Bool {\n        UIDevice.current.userInterfaceIdiom == .phone\n    }\n    \n    static var isIos26: Bool {\n        if #available(iOS 26, *) { true } else { false }\n    }\n\n    static var frameType: DeviceFrameType {\n        if let simulatorModelIdentifier = ProcessInfo().environment[\"SIMULATOR_MODEL_IDENTIFIER\"] {\n            let nameSimulator = simulatorModelIdentifier\n            return .init(deviceName: nameSimulator)\n        }\n        \n        var sysinfo = utsname()\n        uname(&sysinfo) // ignore return value\n        let name = String(\n            bytes: Data(\n                bytes: &sysinfo.machine,\n                count: Int(_SYS_NAMELEN)\n            ),\n            encoding: .ascii\n        )!.trimmingCharacters(in: .controlCharacters)\n        return .init(deviceName: name)\n    }\n}\n\nenum DeviceFrameType {\n    case noNotch, wideNotch, narrowNotch, dynamicIsland\n    \n    init(deviceName: String) {\n        // The number in the device name is 1 higher than the commerical number.\n        switch deviceName {\n        case _ where deviceName.starts(with: /iPhone[1-9][6-9]/): // iPhone 15 and above\n            self = .dynamicIsland\n        case _ where deviceName.starts(with: \"iPhone15\"): // iPhone 14\n            if deviceName == \"iPhone15,2\" || deviceName == \"iPhone15,3\" {\n                self = .dynamicIsland\n            } else {\n                self = .narrowNotch\n            }\n        case _ where deviceName.starts(with: \"iPhone14\"): // iPhone 13\n            self = .narrowNotch\n        case _ where deviceName.starts(with: /iPhone1[1-3]/): // iPhone X - 12\n            self = .wideNotch\n        default:\n            self = .noNotch\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/UIImage+Extensions.swift",
    "content": "//\n//  UIImage+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/07/2024.\n//\n\nimport CoreGraphics\nimport UIKit\nimport Media\n\nextension UIImage {\n    var isPortrait: Bool { size.height > size.width }\n    var isLandscape: Bool { size.width > size.height }\n    var breadth: CGFloat { min(size.width, size.height) }\n    var breadthSize: CGSize { .init(width: breadth, height: breadth) }\n    var breadthRect: CGRect { .init(origin: .zero, size: breadthSize) }\n    \n    static let blank: UIImage = .init()\n    \n    func validSize() -> CGSize? {\n        size == .zero ? nil : size\n    }\n    \n    func boundedAspectRatio(bounds: CoreMediaView.AspectRatioBounds) -> CGSize {\n        // sanity check: bounds do not conflict\n        assert(bounds.boundsAreSane, \"bounds are not sane\")\n        \n        guard size != .zero else { return bounds.defaultSize }\n        \n        switch bounds {\n        case let .bounded(vertical, horizontal):\n            let aspectRatio = size.aspectRatio\n            if let vertical, aspectRatio > vertical.aspectRatio {\n                // if vertically bounded and taller than vertical bounds, clip to vertical bounds\n                return vertical\n            }\n            if let horizontal, aspectRatio < horizontal.aspectRatio {\n                // if horizontally bounded and wider than horizontal bounds, clip to horizontal bounds\n                return horizontal\n            }\n            return size\n        case let .absolute(size):\n            // absolute: just return size\n            return size\n        }\n    }\n    \n    var circleMasked: UIImage {\n        let diameter = min(size.width, size.height)\n        let isLandscape = size.width > size.height\n\n        let xOffset = isLandscape ? (size.width - diameter) / 2 : 0\n        let yOffset = isLandscape ? 0 : (size.height - diameter) / 2\n\n        let imageSize = CGSize(width: diameter, height: diameter)\n\n        return UIGraphicsImageRenderer(size: imageSize).image { _ in\n            let ovalPath = UIBezierPath(ovalIn: CGRect(origin: .zero, size: imageSize))\n            ovalPath.addClip()\n            draw(at: CGPoint(x: -xOffset, y: -yOffset))\n        }\n    }\n    \n    func circleBorder(color: UIColor, width: CGFloat) -> UIImage {\n        let diameter = min(size.width, size.height)\n        let isLandscape = size.width > size.height\n\n        let xOffset = isLandscape ? (size.width - diameter) / 2 : 0\n        let yOffset = isLandscape ? 0 : (size.height - diameter) / 2\n\n        let imageSize = CGSize(width: diameter, height: diameter)\n\n        return UIGraphicsImageRenderer(size: imageSize).image { _ in\n            let ovalPath = UIBezierPath(ovalIn: CGRect(origin: .zero, size: imageSize))\n            ovalPath.addClip()\n            draw(at: CGPoint(x: -xOffset, y: -yOffset))\n           \n            color.setStroke()\n            ovalPath.lineWidth = width\n            ovalPath.stroke()\n        }\n    }\n    \n    func resized(to newSize: CGSize) -> UIImage {\n        let image = UIGraphicsImageRenderer(size: newSize).image { _ in\n            draw(in: CGRect(origin: .zero, size: newSize))\n        }\n        \n        return image.withRenderingMode(renderingMode)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/UITextView+Extensions.swift",
    "content": "//\n//  UITextView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/07/2024.\n//\n\nimport UIKit\n\nextension UITextView {\n    func wrapSelectionWithDelimiters(_ delimiter: String) {\n        wrapSelectionWithDelimiters(leading: delimiter, trailing: delimiter)\n    }\n    \n    func wrapSelectionWithDelimiters(leading leadingDelimiter: String, trailing trailingDelimiter: String) {\n        if let range = selectedTextRange, let text = text(in: range) {\n            if range.start == range.end {\n                let checkStart = position(from: range.start, offset: -leadingDelimiter.count)\n                let checkEnd = position(from: range.end, offset: trailingDelimiter.count)\n                \n                // Checking for delimiters on both sides of the cursor, in which case the delimiters are removed\n                if let checkStart, let checkEnd, let checkRange = textRange(from: checkStart, to: checkEnd) {\n                    if self.text(in: checkRange) == leadingDelimiter + trailingDelimiter {\n                        // Removing delimiters around the cursor\n                        replace(checkRange, withText: \"\")\n                        return\n                    }\n                }\n                \n                // Checking for delimiters on the trailing side, in which case the cursor is moved to after the delimiters\n                if let checkEnd, let checkRange = textRange(from: range.end, to: checkEnd) {\n                    if self.text(in: checkRange) == trailingDelimiter {\n                        selectedTextRange = textRange(from: checkEnd, to: checkEnd)\n                        return\n                    }\n                }\n                \n                // If no surrounding delimiters are detected, add some\n                replace(range, withText: leadingDelimiter + text + trailingDelimiter)\n                if let newPosition = position(from: range.start, offset: leadingDelimiter.count) {\n                    selectedTextRange = textRange(from: newPosition, to: newPosition)\n                }\n            } else {\n                if text.hasPrefix(leadingDelimiter), text.hasSuffix(trailingDelimiter) {\n                    // If delimiters are detected in selection, remove them\n                    replace(range, withText: String(text.dropLast(trailingDelimiter.count).dropFirst(leadingDelimiter.count)))\n                    if let newEnd = position(from: range.end, offset: -(leadingDelimiter.count + trailingDelimiter.count)) {\n                        selectedTextRange = textRange(from: range.start, to: newEnd)\n                    }\n                } else {\n                    // Otherwise, wrap the selection in delimiters\n                    replace(range, withText: leadingDelimiter + text + trailingDelimiter)\n                    if let newEnd = position(from: range.end, offset: leadingDelimiter.count + trailingDelimiter.count) {\n                        selectedTextRange = textRange(from: range.start, to: newEnd)\n                    }\n                }\n            }\n        }\n    }\n    \n    func wrapSelectionWithSpoiler() {\n        insertBlock(prefix: \"::: spoiler \\(String(localized: \"Spoiler\"))\", suffix: \":::\")\n    }\n    \n    func wrapSelectionWithCodeBlock() {\n        insertBlock(prefix: \"```\", suffix: \"```\")\n    }\n    \n    private func insertBlock(prefix: String, suffix: String) {\n        if let range = selectedTextRange, let text = text(in: range) {\n//            let atStart = range.start == beginningOfDocument\n            let atEnd = range.end == endOfDocument\n            let newText = \"\\(prefix)\\n\\(text)\\n\\(suffix)\\(atEnd ? \"\" : \"\\n\")\"\n            replace(range, withText: newText)\n            if let newPosition = position(from: range.start, offset: prefix.count + 1 + text.count) {\n                selectedTextRange = textRange(from: newPosition, to: newPosition)\n            }\n        }\n    }\n    \n    func wrapSelectionWithLink() {\n        let url: URL?\n        if let pastedUrl = UIPasteboard.general.url {\n            url = pastedUrl\n        } else if let pastedString = UIPasteboard.general.string, pastedString.starts(with: \"http\") {\n            url = URL(string: pastedString, encodingInvalidCharacters: false)\n        } else {\n            url = nil\n        }\n        wrapSelectionWithDelimiters(leading: \"[\", trailing: \"](\\(url?.absoluteString ?? \"\"))\")\n    }\n    \n    func toggleQuoteAtCursor() {\n        toggleLinePrefix(prefix: \"> \")\n    }\n    \n    func toggleHeadingAtCursor(level: Int) {\n        guard 1 ... 6 ~= level else {\n            assertionFailure()\n            return\n        }\n        toggleLinePrefix(prefix: String(repeating: \"#\", count: level) + \" \")\n    }\n    \n    // swiftlint:disable:next function_body_length\n    private func toggleLinePrefix(prefix: String) {\n        if let selectedTextRange, let selectedText = text(in: selectedTextRange) {\n            if let firstTargetedNewLineIndex = findLastNewlineIndex(),\n               let lookBehindRange = textRange(from: beginningOfDocument, to: selectedTextRange.start) {\n                // Remove \"> \" if exists\n                var allText = text ?? \"\"\n                if let endIndex = allText.index(firstTargetedNewLineIndex, offsetBy: prefix.count, limitedBy: allText.endIndex) {\n                    if allText[firstTargetedNewLineIndex ..< endIndex] == prefix {\n                        let selectedEndIndex = allText.index(\n                            allText.endIndex,\n                            offsetBy: offset(from: selectedTextRange.end, to: selectedTextRange.end)\n                        )\n                        allText = allText.replacingOccurrences(\n                            of: \"\\n\\(prefix)\", with: \"\\n\", range: firstTargetedNewLineIndex ..< selectedEndIndex\n                        )\n                        allText.removeSubrange(firstTargetedNewLineIndex ..< endIndex)\n                        \n                        var startDistance = 0\n                        if let startIndex = stringIndex(from: selectedTextRange.start) {\n                            // Avoid fatalError from `distance()`\n                            if startIndex > allText.endIndex {\n                                startDistance = prefix.count\n                            } else {\n                                startDistance = allText.distance(from: firstTargetedNewLineIndex, to: startIndex)\n                            }\n                        }\n                        \n                        let newStart = position(\n                            from: selectedTextRange.start,\n                            offset: -min(startDistance, prefix.count)\n                        ) ?? beginningOfDocument\n                        let newEnd = position(\n                            from: selectedTextRange.end,\n                            offset: allText.count - text.count\n                        ) ?? endOfDocument\n                        \n                        text = allText\n                        self.selectedTextRange = textRange(from: newStart, to: newEnd)\n                        return\n                    }\n                }\n                \n                // Insert \"> \" if it doesn't exist\n                \n                guard var lookBehindText = text(in: lookBehindRange) else {\n                    assertionFailure()\n                    return\n                }\n                \n                lookBehindText.insert(contentsOf: prefix, at: firstTargetedNewLineIndex)\n                \n                let newSelectedText = selectedText.replacingOccurrences(of: \"\\n\", with: \"\\n\\(prefix)\")\n                let finalText = lookBehindText + newSelectedText\n                if let finalRange = textRange(from: beginningOfDocument, to: selectedTextRange.end) {\n                    replace(finalRange, withText: finalText)\n                    \n                    let newStart = position(\n                        from: selectedTextRange.start,\n                        offset: prefix.count\n                    ) ?? beginningOfDocument\n                    let newEnd = position(\n                        from: selectedTextRange.end,\n                        offset: (newSelectedText.count - selectedText.count) + prefix.count\n                    ) ?? endOfDocument\n                    self.selectedTextRange = textRange(from: newStart, to: newEnd)\n                }\n            }\n        }\n    }\n    \n    // MARK: Helper functions\n    \n    private func findLastNewlineIndex() -> String.Index? {\n        if let start = selectedTextRange?.start, let lookBehindRange = textRange(from: beginningOfDocument, to: start),\n           let lookBehindText = text(in: lookBehindRange) {\n            if let newlineIndex = lookBehindText.lastIndex(of: \"\\n\") {\n                return lookBehindText.index(newlineIndex, offsetBy: 1, limitedBy: lookBehindText.endIndex)\n            } else {\n                return lookBehindText.startIndex\n            }\n        }\n        return nil\n    }\n\n    private func stringIndex(from textPosition: UITextPosition) -> String.Index? {\n        guard let text else { return nil }\n        let offset = offset(from: beginningOfDocument, to: textPosition)\n        guard offset >= 0, offset <= text.utf16.count else { return nil }\n        return String.Index(utf16Offset: offset, in: text)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/UIUserInterfaceStyle+Extensions.swift",
    "content": "//\n//  UIUserInterfaceStyle+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-31.\n//\n\nimport Foundation\nimport Icons\nimport SwiftUI\nimport UIKit\n\nextension UIUserInterfaceStyle: @retroactive Codable {}\nextension UIUserInterfaceStyle {\n    var label: String {\n        switch self {\n        case .unspecified:\n            \"System\"\n        case .light:\n            \"Light\"\n        case .dark:\n            \"Dark\"\n        default:\n            \"Unknown\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .unspecified: .settings.systemMode\n        case .light: .settings.lightMode\n        case .dark: .settings.darkMode\n        default: .settings.systemMode\n        }\n    }\n    \n    var colorScheme: ColorScheme? {\n        switch self {\n        case .light: .light\n        case .dark: .dark\n        default: nil\n        }\n    }\n    \n    static var optionCases: [UIUserInterfaceStyle] {\n        [.unspecified, .light, .dark]\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/UIViewController+Extensions.swift",
    "content": "//\n//  UIViewController+TopMostViewController.swift\n//  Mlem\n//\n//  Created by mormaer on 19/07/2023.\n//\n//\n\nimport UIKit\n\nextension UIViewController {\n    func topMostViewController() -> UIViewController {\n        if let presented = presentedViewController {\n            return presented.topMostViewController()\n        }\n        \n        if let navigation = self as? UINavigationController {\n            return navigation.visibleViewController?.topMostViewController() ?? navigation\n        }\n        \n        if let tab = self as? UITabBarController {\n            return tab.selectedViewController?.topMostViewController() ?? tab\n        }\n        \n        return self\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/UsernameValidity+Extensions.swift",
    "content": "//\n//  UsernameValidity+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-05-24.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nextension UsernameValidity {\n    var label: LocalizedStringResource {\n        switch self {\n        case .available: \"Available\"\n        case .taken: \"Username is taken.\"\n        case let .invalid(reason): reason.label\n        }\n    }\n}\n\nextension UsernameValidity.InvalidityReason {\n    var label: LocalizedStringResource {\n        switch self {\n        case let .tooShort(minLength: minLength):\n            return \"Username must be at least \\(minLength) characters long.\"\n        case let .tooLong(maxLength: maxLength):\n            return \"Username cannot be longer than \\(maxLength) characters.\"\n        case let .containsInvalidCharacters(characters):\n            let characterList = characters.map { \"\\\"\\($0)\\\"\" }.formatted(.list(type: .or))\n            return \"Username cannot contain \\(characterList).\"\n        case .other:\n            return \"Username is invalid.\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/Label+Profile1.swift",
    "content": "//\n//  Label+Profile1.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-11-11.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension Label {\n    init(_ model: ProfileProviding) where Title == Text, Icon == SimpleAvatarView {\n        self.init {\n            Text(model.name)\n        } icon: {\n            SimpleAvatarView(url: model.avatar, type: type(of: model).avatarFallback)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/NavigationLink+NavigationPage.swift",
    "content": "//\n//  NavigationLink+NavigationPage.swift\n//  Mlem\n//\n//  Created by Sjmarf on 07/05/2024.\n//\n\nimport Icons\nimport SwiftUI\n\nextension NavigationLink where Destination == Never {\n    init(_ value: NavigationPage, @ViewBuilder label: () -> Label) {\n        self.init(value: value, label: label)\n    }\n    \n    init(_ titleKey: LocalizedStringResource, destination: NavigationPage) where Label == Text {\n        self.init(value: destination) { Text(titleKey) }\n    }\n    \n    init(\n        _ titleKey: LocalizedStringResource,\n        value: String,\n        fallbackValue: String,\n        icon: Icon? = nil,\n        destination: NavigationPage\n    ) where Label == NavigationLinkPickerLabelView {\n        self.init(destination) {\n            NavigationLinkPickerLabelView(\n                title: .init(localized: titleKey),\n                value: value,\n                fallbackValue: fallbackValue,\n                icon: icon\n            )\n        }\n    }\n    \n    @_disfavoredOverload\n    init(_ title: String, destination: NavigationPage) where Label == Text {\n        self.init(value: destination) { Text(title) }\n    }\n    \n    init(\n        _ titleKey: LocalizedStringResource,\n        icon: Icon,\n        destination: NavigationPage\n    ) where Label == SwiftUI.Label<Text, Image> {\n        self.init(value: destination) { Label(String(localized: titleKey), icon: icon) }\n    }\n}\n\nstruct NavigationLinkPickerLabelView: View {\n    let title: String\n    let value: String\n    let fallbackValue: String\n    let icon: Icon?\n    \n    var body: some View {\n        HStack(spacing: 10) {\n            Group {\n                if let icon {\n                    Label(title, icon: icon)\n                } else {\n                    Text(title)\n                }\n            }\n            .frame(maxWidth: .infinity, alignment: .leading)\n\n            ViewThatFits {\n                Text(value)\n                Text(fallbackValue)\n            }\n            .foregroundStyle(.themedSecondary)\n            .lineLimit(1)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/PreviewModifier+SampleEnvironment.swift",
    "content": "//\n//  PreviewModifier+SampleEnvironment.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-02.\n//\n\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\n// TODO: updated mocks\n// #if DEBUG\n//    private struct SampleEnvironmentPreviewModifier: PreviewModifier {\n//        // Kinda unfortunate typealias naming considering we have our own AppState...\n//        typealias AppState = Void\n//    \n//        var api: MockApiClient = .mock\n//    \n//        static func makeSharedContext() async throws -> AppState {\n//            // no-op\n//        }\n//    \n//        func body(content: Content, context: AppState) -> some View {\n//            content\n//                .environment(NavigationLayer(root: .blockList, model: .main))\n//                .environment(Mlem.AppState.mock(api: api))\n//                .environment(FiltersTracker.main)\n//                .environment(TabReselectTracker.main)\n//                .environment(BackendClient.main)\n//                .environment(HapticManager.main)\n//        }\n//    }\n//\n//    extension PreviewTrait where T == Preview.ViewTraits {\n//        static var sampleEnvironment: PreviewTrait {\n//            if #available(iOS 18.0, *) {\n//                return .modifier(SampleEnvironmentPreviewModifier())\n//            } else {\n//                return .defaultLayout\n//            }\n//        }\n//    \n//        static func sampleEnvironment(api: MockApiClient) -> PreviewTrait {\n//            if #available(iOS 18.0, *) {\n//                return .modifier(SampleEnvironmentPreviewModifier(api: api))\n//            } else {\n//                return .defaultLayout\n//            }\n//        }\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/PopupAnchorModel.swift",
    "content": "//\n//  PopupAnchorModel.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-14.\n//\n\nimport Foundation\n\n@Observable\nclass PopupAnchorModel {\n    struct PopupData {\n        var message: String\n        var actions: [Action]?\n    }\n\n    enum Outcome {\n        case cancelled, confirmed\n    }\n    \n    struct Action {\n        let title: String\n        let isDestructive: Bool\n        let callback: @MainActor () -> Void\n        \n        init(title: LocalizedStringResource, isDestructive: Bool = false, callback: @escaping () -> Void) {\n            self.title = .init(localized: title)\n            self.isDestructive = isDestructive\n            self.callback = callback\n        }\n        \n        @_disfavoredOverload\n        init(title: some StringProtocol, isDestructive: Bool = false, callback: @escaping () -> Void) {\n            self.title = String(title)\n            self.isDestructive = isDestructive\n            self.callback = callback\n        }\n    }\n    \n    private(set) var data: PopupData?\n    var outcome: Outcome?\n    \n    func showPopup(message: LocalizedStringResource, _ actions: [Action]?) {\n        showPopup(message: .init(localized: message), actions)\n    }\n    \n    @_disfavoredOverload\n    func showPopup(message: String, _ actions: [Action]?) {\n        let newData = PopupData(message: message, actions: actions)\n        if data == nil {\n            data = newData\n        } else {\n            data = nil\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {\n                self.data = newData\n            }\n        }\n    }\n        \n    func dismissPopup() {\n        data = nil\n    }\n}\n\nextension PopupAnchorModel {\n    func showPopup(_ actionGroup: ActionGroup) {\n        let children: [PopupAnchorModel.Action] = actionGroup.children.map { child in\n            .init(title: child.appearance.label, isDestructive: child.appearance.isDestructive) { @MainActor in\n                if let child = child as? BasicAction {\n                    child.callbackWithConfirmation(popupModel: self)\n                } else {\n                    assertionFailure(\"Not implemented\")\n                }\n            }\n        }\n        showPopup(\n            message: actionGroup.prompt ?? actionGroup.appearance.label,\n            children\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+AccountSwitcherGesture.swift",
    "content": "//\n//  View+AccountSwitcherGesture.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-09-03.\n//\n\nimport SwiftUI\n\nstruct AccountSwitcherGesture: ViewModifier {\n    let tabReselectTracker: TabReselectTracker\n    let navigationModel: NavigationModel\n    \n    @GestureState private var dragGestureActive: Bool = false\n    @State var switcherOpened: Bool = false\n    @State var dragCompleted: Bool = false\n    \n    func body(content: Content) -> some View {\n        if #available(iOS 26, *), !UIDevice.isPad {\n            content\n                .simultaneousGesture(DragGesture()\n                    .updating($dragGestureActive) { _, state, _ in\n                        state = true\n                    }\n                    .onChanged { value in\n                        if (UIScreen.main.bounds.height - value.startLocation.y) < 80,\n                           value.translation.height < -100,\n                           !switcherOpened {\n                            switcherOpened = true\n                            tabReselectTracker.blockTabSwitch = true\n                            navigationModel.openSheet(.quickSwitcher)\n                            \n                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {\n                                tabReselectTracker.blockTabSwitch = false\n                            }\n                        }\n                    })\n                .onChange(of: dragGestureActive) {\n                    if !dragGestureActive {\n                        switcherOpened = false\n                    }\n                }\n        } else {\n            content\n        }\n    }\n}\n\nextension View {\n    func withAccountSwitcherGesture(tabReselectTracker: TabReselectTracker, navigationModel: NavigationModel) -> some View {\n        modifier(AccountSwitcherGesture(tabReselectTracker: tabReselectTracker, navigationModel: navigationModel))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+Background.swift",
    "content": "//\n//  View+Background.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-04-01.\n//\n\nimport SwiftUI\nimport Theming\n\nextension View {\n    func themedGroupedBackground() -> some View {\n        Group {\n            if #available(iOS 18.0, *) {\n                containerBackground(.themedGroupedBackground, for: .navigation)\n            } else {\n                background(ThemedColor.themedGroupedBackground, in: Rectangle())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+ConditionalNavigationTitle.swift",
    "content": "//\n//  View+onditionalNavigationTitle.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-30.\n//\n\nimport SwiftUI\n\nprivate struct ConditionalNavigationTitle: ViewModifier {\n    let title: String\n    \n    @State private var isAtTop: Bool = true\n\n    func body(content: Content) -> some View {\n        content\n            .isAtTopSubscriber(isAtTop: $isAtTop)\n            // Unfortunately `.toolbar(removing: )` doesn't work with a condition :(\n            .navigationTitle(isAtTop ? \"\" : title)\n    }\n}\n\nextension View {\n    func conditionalNavigationTitle(_ title: LocalizedStringResource) -> some View {\n        modifier(ConditionalNavigationTitle(title: .init(localized: title)))\n    }\n    \n    @_disfavoredOverload\n    func conditionalNavigationTitle(_ title: String) -> some View {\n        modifier(ConditionalNavigationTitle(title: title))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+ContentMenu.swift",
    "content": "//\n//  View+ContentMenu.swift\n//  Mlem\n//\n//  Created by Sjmarf on 23/06/2024.\n//\n\nimport SwiftUI\n\nextension View {\n    func contextMenu(actions: [any Action]) -> some View {\n        contextMenu {\n            ForEach(actions, id: \\.id) { action in\n                MenuButton(action: action)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+ContextMenu.swift",
    "content": "//\n//  View+ContextMenu.swift\n//  Mlem\n//\n//  Created by Sjmarf on 23/06/2024.\n//\n\nimport SwiftUI\n\n// This setup avoids actually generating the array of actions until the context menu itself\n// is opened. This can have performance benefits in certain situations.\n\nextension View {\n    @ViewBuilder\n    func contextMenu(@ActionBuilder actions: @escaping () -> [any Action]) -> some View {\n        contextMenu {\n            // Having a proper view here is necessary - if `ForEach` is used directly, `actions()` gets called early.\n            MenuButtons(actions: actions)\n        }\n        .popupAnchor()\n    }\n}\n\nstruct MenuButtons: View {\n    @ActionBuilder let actions: () -> [any Action]\n\n    var body: some View {\n        ForEach(actions(), id: \\.id) { action in\n            MenuButton(action: action)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+DynamicBlur.swift",
    "content": "//\n//  View+DynamicBlur.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-22.\n//\n\nimport Foundation\nimport SwiftUI\n\nprivate struct DynamicBlur: ViewModifier {\n    @State var blurValue: CGFloat = 100\n    let blurred: Bool\n    \n    func body(content: Content) -> some View {\n        content\n            .blur(radius: blurred ? blurValue : 0, opaque: true)\n            .background {\n                GeometryReader { geo in\n                    Color.clear.contentShape(.rect)\n                        .frame(maxWidth: .infinity, maxHeight: .infinity)\n                        .onChange(of: geo.size, initial: true) {\n                            blurValue = max(geo.size.width, geo.size.height) / 12\n                        }\n                }\n            }\n    }\n}\n\nextension View {\n    /// Blurs an image relative to its size\n    func dynamicBlur(blurred: Bool) -> some View {\n        modifier(DynamicBlur(blurred: blurred))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+ExternalApiWarning.swift",
    "content": "//\n//  View+ExternalApiWarning.swift\n//  Mlem\n//\n//  Created by Sjmarf on 02/06/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nprivate struct ExternalApiWarningModifier: ViewModifier {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    let entity: any ContentModel & ActorIdentifiable\n    let isLoading: Bool\n\n    func body(content: Content) -> some View {\n        content\n            .safeAreaInset(edge: .top) {\n                if !isLoading, !entity.api.isActive(appState: appState) {\n                    label\n                        .padding(8)\n                        .frame(maxWidth: .infinity)\n                        // Using colors directly here causes the background to extend\n                        // into the navbar, so use filled rectangles instead\n                        .background {\n                            Rectangle()\n                                .fill(.themedAccent.opacity(0.2))\n                        }\n                        .background {\n                            Rectangle()\n                                .fill(.thickMaterial)\n                        }\n                        .padding(.bottom, 3)\n                }\n            }\n    }\n    \n    var label: some View {\n        HStack {\n            Text(title)\n                .foregroundStyle(.themedPrimary.opacity(0.5))\n            Spacer()\n            Button(\"More Info\", systemImage: \"questionmark.circle\") {\n                navigation.openSheet(.externalApiInfo(\n                    api: entity.api,\n                    actorId: entity.actorId\n                ))\n            }\n            .labelStyle(.iconOnly)\n        }\n    }\n    \n    var title: AttributedString {\n        let host = entity.api.host\n        var attributedString = AttributedString(localized: \"Viewing \\(host) as guest\")\n        \n        if let range = attributedString.range(of: host) {\n            attributedString[range].font = .body.weight(.semibold)\n        }\n        return attributedString\n    }\n}\n\nextension View {\n    func externalApiWarning(entity: any ContentModel & ActorIdentifiable, isLoading: Bool) -> some View {\n        modifier(ExternalApiWarningModifier(entity: entity, isLoading: isLoading))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+HiddenNavigationTitle.swift",
    "content": "//\n//  View+HiddenNavigationTitle.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-18.\n//\n\nimport SwiftUI\n\nextension View {\n    @ViewBuilder\n    func hiddenNavigationTitle(_ title: LocalizedStringResource) -> some View {\n        hiddenNavigationTitle(String(localized: title))\n    }\n    \n    @_disfavoredOverload\n    @ViewBuilder\n    func hiddenNavigationTitle(_ title: String) -> some View {\n        if #available(iOS 18.0, *) {\n            self\n                .navigationTitle(title)\n                .toolbar(removing: .title)\n        } else {\n            self\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+IsAtTopSubscriber.swift",
    "content": "//\n//  View+IsAtTopSubscriber.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-14.\n//\n\nimport Foundation\nimport SwiftUI\n\nprivate struct IsAtTopSubscriber: ViewModifier {\n    @Binding var isAtTop: Bool\n    \n    func body(content: Content) -> some View {\n        content\n            .onPreferenceChange(IsAtTopPreferenceKey.self, perform: { value in\n                if value != isAtTop {\n                    isAtTop = value\n                }\n            })\n    }\n}\n\nextension View {\n    /// Updates a given bool according to the IsAtTopPreferenceKey\n    func isAtTopSubscriber(isAtTop: Binding<Bool>) -> some View {\n        modifier(IsAtTopSubscriber(isAtTop: isAtTop))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+LoadFeed.swift",
    "content": "//\n//  View+LoadFeed.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-07-05.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nprivate struct LoadFeed: ViewModifier {\n    @Setting(\\.post_size) var postSize\n    \n    let feedLoader: (any FeedLoading)?\n    let shouldLoad: Bool\n    let errorDetails: Binding<ErrorDetails?>?\n    \n    func body(content: Content) -> some View {\n        content\n            .onChange(of: onChangeHash, initial: true) {\n                if let feedLoader, shouldLoad, feedLoader.loadingState == .initial {\n                    // wrapping this in a Task instead of using .task prevents cancellation errors from the parent view de-rendering\n                    Task {\n                        do {\n                            try await feedLoader.loadMoreItems()\n                            errorDetails?.wrappedValue = nil\n                        } catch {\n                            handleLoadFailure(error)\n                        }\n                    }\n                }\n            }\n            .onChange(of: postSize) {\n                (feedLoader as? CorePostFeedLoader)?.setPrefetchingConfiguration(.forPostSize(postSize))\n            }\n    }\n\n    func handleLoadFailure(_ error: any Error) {\n        if let errorDetailsBinding = self.errorDetails {\n            if var details = handleErrorWithDetails(error) {\n                details.refresh = {\n                    do {\n                        try await feedLoader?.loadMoreItems()\n                        return true\n                    } catch {\n                        return false\n                    }\n                }\n                errorDetailsBinding.wrappedValue = details\n            }\n        } else {\n            handleError(error)\n        }\n    }\n    \n    var onChangeHash: Int {\n        var hasher = Hasher()\n        hasher.combine(feedLoader == nil)\n        hasher.combine(shouldLoad)\n        return hasher.finalize()\n    }\n}\n\nextension View {\n    /// Convenience modifier. Attach to a view to load items from the given FeedLoading on appear if the given FeedLoading has no items\n    func loadFeed(\n        _ feedLoader: (any FeedLoading)?,\n        shouldLoad: Bool = true,\n        errorDetails: Binding<ErrorDetails?>? = nil\n    ) -> some View {\n        modifier(LoadFeed(\n            feedLoader: feedLoader,\n            shouldLoad: shouldLoad,\n            errorDetails: errorDetails\n        ))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+MarkReadOnScroll.swift",
    "content": "//\n//  View+MarkReadOnScroll.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-28.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nprivate struct MarkReadOnScroll: ViewModifier {\n    @Setting(\\.feed_markReadOnScroll) var markReadOnScroll\n    @Setting(\\.post_size) var postSize\n    \n    var index: Int\n    var post: Post\n    var postFeedLoader: CorePostFeedLoader\n    @Binding var bottomAppearedItemIndex: Int\n    \n    func body(content: Content) -> some View {\n        if #available(iOS 18.0, *) {\n            ios18Body(content: content)\n        } else {\n            legacyBody(content: content)\n        }\n    }\n    \n    @available(iOS 18.0, *)\n    func ios18Body(content: Content) -> some View {\n        content\n            .onGeometryChange(for: Bool.self) { geometry in\n                geometry.frame(in: .global).maxY < 90\n            } action: { wasAboveTop, isAboveTop in\n                if markReadOnScroll, !wasAboveTop, isAboveTop {\n                    post.updateRead(true, shouldQueue: true)\n                }\n            }\n    }\n    \n    func legacyBody(content: Content) -> some View {\n        content\n            .task {\n                if markReadOnScroll {\n                    bottomAppearedItemIndex = max(index, bottomAppearedItemIndex)\n                }\n            }\n            .onDisappear {\n                if markReadOnScroll, // mark read on scroll enabled\n                   index <= (bottomAppearedItemIndex - postSize.markReadOffset) ||\n                   index >= (postFeedLoader.items.count - postSize.markReadOffset) { // edge case: end of feed\n                    post.updateRead(true, shouldQueue: true)\n                }\n            }\n    }\n}\n\nextension View {\n    /// Handles mark read on scroll behavior:\n    /// - On appear, stages previous posts to be marked read\n    /// - On disappear, if this post is staged, marks it as read\n    func markReadOnScroll(\n        index: Int,\n        post: Post,\n        postFeedLoader: CorePostFeedLoader,\n        bottomAppearedItemIndex: Binding<Int>\n    ) -> some View {\n        modifier(MarkReadOnScroll(\n            index: index,\n            post: post,\n            postFeedLoader: postFeedLoader,\n            bottomAppearedItemIndex: bottomAppearedItemIndex\n        ))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+NavigationTransition.swift",
    "content": "//\n//  View+NavigationTransition.swift\n//  Mlem\n//\n//  Created by Sjmarf on 29/08/2024.\n//\n\nimport SwiftUI\n\nextension View {\n    func navigationTransition_(sourceID: any Hashable, in namespace: Namespace.ID?) -> some View {\n        // The code below requires Xcode 16, and is intentionally left commented for now\n        // until we upgrade to Xcode 16.\n        self\n//        Group {\n//            if #available(iOS 18.0, *), let namespace {\n//                self.navigationTransition(.zoom(sourceID: sourceID, in: namespace))\n//            } else {\n//                self\n//            }\n//        }\n    }\n    \n    func matchedTransitionSource_(id: some Hashable, in namespace: Namespace.ID) -> some View {\n        // The code below requires Xcode 16, and is intentionally left commented for now\n        // until we upgrade to Xcode 16.\n        self\n//        Group {\n//            if #available(iOS 18.0, *) {\n//                self.matchedTransitionSource(id: id, in: namespace, configuration: { config in\n//                    config.clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n//                })\n//            } else {\n//                self\n//            }\n//        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+NavigtionStackPreview.swift",
    "content": "//\n//  View+NavigtionStackPreview.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-13.\n//\n\nimport SwiftUI\n\n#if DEBUG\n    // This can be used in previews to show a back button in the corner\n    private struct NavigationStackPreviewModifier: ViewModifier {\n        let backButtonLabel: String\n    \n        func body(content: Content) -> some View {\n            NavigationStack(path: .constant([1])) {\n                Color.clear // If EmptyView() is used here, the back button isn't labelled correctly\n                    .navigationTitle(backButtonLabel)\n                    .navigationDestination(for: Int.self) { _ in content }\n            }\n        }\n    }\n\n    extension View {\n        func previewNavigationStack(backButtonLabel: LocalizedStringResource = \"Back\") -> some View {\n            modifier(NavigationStackPreviewModifier(backButtonLabel: .init(localized: backButtonLabel)))\n        }\n    }\n#endif\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+OutdatedFeedPopup.swift",
    "content": "//\n//  View+OutdatedFeedPopup.swift\n//  Mlem\n//\n//  Created by Sjmarf on 03/08/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nprivate struct OutdatedFeedPopupModifier: ViewModifier {\n    @Environment(AppState.self) var appState\n    @Environment(FiltersTracker.self) var filtersTracker\n    \n    let feedLoader: (any FeedLoading)?\n    \n    let canShowPopup: Bool\n    let onManualRefresh: (() -> Void)?\n\n    init(feedLoader: (any FeedLoading)?, showPopup canShowPopup: Bool, onManualRefresh: (() -> Void)? = nil) {\n        self.feedLoader = feedLoader\n        self.canShowPopup = canShowPopup\n        self.onManualRefresh = onManualRefresh\n    }\n    \n    @State var showRefreshPopup: Bool = false\n    \n    func body(content: Content) -> some View {\n        content\n            .refreshable(isEnabled: feedLoader != nil) {\n                if let feedLoader {\n                    await refresh(feedLoader, clearBeforeRefresh: false)\n                    onManualRefresh?()\n                }\n            }\n            .onChange(of: apiChangeHash) {\n                if let feedLoader {\n                    if let newApi = feedLoader.items.first?.api {\n                        showRefreshPopup = canShowPopup && (\n                            newApi !== appState.firstApi && ![.loading, .initial].contains(feedLoader.loadingState)\n                        )\n                    } else {\n                        showRefreshPopup = false\n                    }\n                }\n            }\n            .onChange(of: filtersTracker.changeHash) {\n                if let feedLoader {\n                    if feedLoader.items.count > 0 {\n                        showRefreshPopup = true\n                    }\n                }\n            }\n            .overlay(alignment: .bottom) {\n                RefreshPopupView(\"Feed is outdated\", isPresented: $showRefreshPopup) {\n                    Task {\n                        if let feedLoader {\n                            await refresh(feedLoader, clearBeforeRefresh: true)\n                        }\n                    }\n                }\n            }\n    }\n    \n    var apiChangeHash: Int {\n        var hasher = Hasher()\n        hasher.combine(canShowPopup)\n        hasher.combine(appState.firstApi)\n        hasher.combine(feedLoader?.loadingState)\n        hasher.combine(feedLoader?.items.first?.api)\n        return hasher.finalize()\n    }\n    \n    func refresh(_ feedLoader: any FeedLoading, clearBeforeRefresh: Bool) async {\n        do {\n            showRefreshPopup = false\n            await feedLoader.changeApi(to: appState.firstApi, context: filtersTracker.filterContext)\n            \n            // This duplication isn't ideal, but it works for now\n            if let feedLoader = feedLoader as? AggregatePostFeedLoader {\n                if try await !appState.firstApi.supports(.postSortType(feedLoader.sortType)) {\n                    try await feedLoader.changeSortType(to: appState.initialFeedSortType, forceRefresh: true)\n                    return\n                }\n            }\n            if let feedLoader = feedLoader as? CommunityPostFeedLoader {\n                if try await !appState.firstApi.supports(.postSortType(feedLoader.sortType)) {\n                    try await feedLoader.changeSortType(to: appState.initialFeedSortType, forceRefresh: true)\n                    return\n                }\n            }\n            \n            try await feedLoader.refresh(clearBeforeRefresh: clearBeforeRefresh)\n        } catch {\n            handleError(error)\n        }\n    }\n}\n\nextension View {\n    func outdatedFeedPopup(feedLoader: (any FeedLoading)?, showPopup: Bool = true, onManualRefresh: (() -> Void)? = nil) -> some View {\n        modifier(OutdatedFeedPopupModifier(feedLoader: feedLoader, showPopup: showPopup, onManualRefresh: onManualRefresh))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+PaletteBorder.swift",
    "content": "//\n//  View+PaletteBorder.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-10-10.\n//\n\nimport Foundation\nimport SwiftUI\n\nprivate struct PaletteBorder: ViewModifier {\n    @Environment(\\.palette) var palette\n    \n    var cornerRadius: CGFloat\n    \n    func body(content: Content) -> some View {\n        content\n            .overlay {\n                if palette.bordered {\n                    RoundedRectangle(cornerRadius: cornerRadius)\n                        .stroke(.themedDivider, lineWidth: 0.5)\n                }\n            }\n    }\n}\n\nextension View {\n    /// Applies a rounded rect border to the view if the current palette `.bordered` is `true`\n    func paletteBorder(cornerRadius: CGFloat) -> some View {\n        modifier(PaletteBorder(cornerRadius: cornerRadius))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+PopupAnchor.swift",
    "content": "//\n//  View+PopupAnchor.swift\n//  Mlem\n//\n//  Created by Sjmarf on 18/09/2024.\n//\n\nimport Icons\nimport SwiftUI\n\nstruct PopupAnchor: ViewModifier {\n    @State var model: PopupAnchorModel\n    \n    var actions: [PopupAnchorModel.Action] {\n        model.data?.actions ?? []\n    }\n    \n    var isPresented: Binding<Bool> {\n        Binding(\n            get: { model.data != nil },\n            set: {\n                if !$0 { model.dismissPopup() }\n            }\n        )\n    }\n    \n    func body(content: Content) -> some View {\n        if #available(iOS 26, *) {\n            content\n                .alert(\n                    model.data?.message ?? \"\",\n                    isPresented: isPresented\n                ) {\n                    buttonsView\n                }\n                .environment(model)\n        } else {\n            content\n                .confirmationDialog(\n                    model.data?.message ?? \"\",\n                    isPresented: isPresented\n                ) {\n                    buttonsView\n                } message: {\n                    Text(model.data?.message ?? \"\")\n                }\n                .environment(model)\n        }\n    }\n    \n    @ViewBuilder\n    var buttonsView: some View {\n        ForEach(Array(actions.enumerated()), id: \\.offset) { _, action in\n            Button(\n                action.title,\n                role: action.isDestructive ? .destructive : nil\n            ) {\n                action.callback()\n                model.outcome = .confirmed\n            }\n        }\n        Button(\"Cancel\", role: .cancel) {\n            model.outcome = .cancelled\n        }\n    }\n}\n\nextension View {\n    @ViewBuilder\n    func popupAnchor(model: PopupAnchorModel = .init()) -> some View {\n        modifier(PopupAnchor(model: model))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+QuickSwipes.swift",
    "content": "//\n//  View+QuickSwipes.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-23.\n//\n\nimport MlemMiddleware\nimport QuickSwipes\nimport SwiftUI\n\nprivate struct QuickSwipeEnvironmentReaderViewModifier: ViewModifier {\n    @Environment(\\.self) var environment\n    \n    var buildConfiguration: (EnvironmentValues) -> SwipeConfiguration\n    \n    init(_ buildConfiguration: @escaping (EnvironmentValues) -> SwipeConfiguration) {\n        self.buildConfiguration = buildConfiguration\n    }\n    \n    func body(content: Content) -> some View {\n        content.quickSwipes(buildConfiguration(environment))\n    }\n}\n\nextension View {\n    @ViewBuilder\n    func quickSwipes(\n        leading: [any Action] = [],\n        trailing: [any Action] = []\n    ) -> some View {\n        quickSwipes(.init(leadingActions: leading, trailingActions: trailing))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+Refreshable.swift",
    "content": "//\n//  View+Refreshable.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-18.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct RefreshableWrapperView: ViewModifier {\n    let isEnabled: Bool\n\n    let refreshAction: @Sendable () async -> Void\n\n    func body(content: Content) -> some View {\n        RefreshToggling(isEnabled: isEnabled, content: content)\n            .refreshable(action: refreshAction)\n    }\n}\n\nprivate struct RefreshToggling<Content: View>: View {\n    @Environment(\\.refresh) private var refresh\n    let isEnabled: Bool\n\n    let content: Content\n\n    var body: some View {\n        content\n            .environment(EnvironmentValues.safeWritableRefreshKeyPath, isEnabled ? refresh : nil)\n    }\n}\n\nprivate struct RefreshCastFailsafeKey: EnvironmentKey {\n    static let defaultValue: RefreshAction? = nil\n}\n\nprivate extension EnvironmentValues {\n    static let safeWritableRefreshKeyPath: WritableKeyPath<EnvironmentValues, RefreshAction?> = {\n        guard let keyPath = \\EnvironmentValues.refresh as? WritableKeyPath<EnvironmentValues, RefreshAction?> else {\n            handleError(MlemError.modelError(\"Using refreshFailsafe - .refreshable isn't working!\"), silent: true)\n            return \\.refreshFailsafe\n        }\n        return keyPath\n    }()\n\n    var refreshFailsafe: RefreshAction? {\n        get { self[RefreshCastFailsafeKey.self] }\n        set { self[RefreshCastFailsafeKey.self] = newValue }\n    }\n}\n\npublic extension View {\n    func refreshable(isEnabled: Bool, _ operation: @escaping @Sendable () async -> Void) -> some View {\n        modifier(RefreshableWrapperView(isEnabled: isEnabled, refreshAction: operation))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+ReloadOnAccountSwitch.swift",
    "content": "//\n//  View+ReloadOnAccountSwitch.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-02-10.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nprivate struct ReloadOnAccountSwitchModifier<T: UnifiedModelProviding & ContentIdentifiable>: ViewModifier {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    @Binding var entity: T\n    @Binding var isLoading: Bool\n    var callback: ((T) -> Void)?\n\n    func body(content: Content) -> some View {\n        content\n            .onChange(of: appState.firstApi) {\n                isLoading = true\n                Task {\n                    do {\n                        let newEntity = try await entity.resolve(with: appState.firstApi)\n                        callback?(newEntity)\n                        Task { @MainActor in\n                            entity = newEntity\n                            isLoading = false\n                        }\n                    } catch {\n                        handleError(error)\n                        Task { @MainActor in isLoading = false }\n                    }\n                }\n            }\n    }\n}\n\nextension View {\n    func reloadOnAccountSwitch<T: UnifiedModelProviding & ContentIdentifiable>(\n        entity: Binding<T>,\n        isLoading: Binding<Bool>,\n        callback: ((T) -> Void)? = nil) -> some View {\n        modifier(ReloadOnAccountSwitchModifier(entity: entity, isLoading: isLoading, callback: callback))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+SafeAreaBar.swift",
    "content": "//\n//  View+SafeAreaBar.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-04.\n//\n\nimport SwiftUI\n\nextension View {\n    @ViewBuilder\n    func safeAreaBar_(edge: VerticalEdge, @ViewBuilder content: () -> some View) -> some View {\n        if #available(iOS 26.0, *) {\n            safeAreaBar(edge: edge, content: content)\n        } else {\n            safeAreaInset(edge: edge, content: content)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+TabBarPreview.swift",
    "content": "//\n//  View+TabBarPreview.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-23.\n//\n\nimport Foundation\nimport SwiftUI\n\n#if DEBUG\n    struct TabBarPreviewModifier: ViewModifier {\n        @Environment(AppState.self) var appState\n    \n        var selected: ContentView.Tab\n    \n        func body(content: Content) -> some View {\n            TabView(selection: .constant(selected)) {\n                ForEach(ContentView.Tab.allCases, id: \\.self) { type in\n                    content\n                        .tag(type)\n                        .tabItem {\n                            Label(\n                                type.label(appState: appState, profileLabelType: .anonymous),\n                                icon: type.icon.representingState(active: selected == type)\n                            )\n                        }\n                }\n            }\n        }\n    }\n\n    extension View {\n        func previewTabBar(selected: ContentView.Tab) -> some View {\n            modifier(TabBarPreviewModifier(selected: selected))\n        }\n    }\n#endif\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+TabReselectConsumer.swift",
    "content": "//\n//  View+TabReselectionConsumer.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-11.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct TabReselectionConsumer: ViewModifier {\n    @Environment(TabReselectTracker.self) var tabReselectTracker\n\n    /// Reselect actions should only trigger when the view is shown, so we track it with this\n    @State var displayed: Bool = false\n    \n    var action: () -> Void\n\n    func body(content: Content) -> some View {\n        content\n            .onChange(of: tabReselectTracker.flag) {\n                // Only execute the action if:\n                // - This view is currently displayed (this prevents it from triggering while in a different tab)\n                // - Flag is true--combined with the reset() call below, this ensures that only one consumer will consume this action, preventing the behavior where a \"dismiss\" action also scrolls the previous page\n                if displayed, tabReselectTracker.flag {\n                    tabReselectTracker.reset()\n                    action()\n                }\n            }\n            .onAppear {\n                if !displayed {\n                    displayed = true\n                    tabReselectTracker.consumers += 1\n                }\n            }\n            .onDisappear {\n                if displayed {\n                    displayed = false\n                    tabReselectTracker.consumers -= 1\n                }\n            }\n    }\n}\n\nextension View {\n    func onReselectTab(action: @escaping () -> Void) -> some View {\n        modifier(TabReselectionConsumer(action: action))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/Views/View Modifiers/View+WidthReader.swift",
    "content": "//\n//  View+WidthReader.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-07-28.\n//\n\nimport Foundation\nimport SwiftUI\n\nprivate struct WidthReader: ViewModifier {\n    @Binding var width: CGFloat\n    \n    func body(content: Content) -> some View {\n        content\n            .background {\n                GeometryReader { geo in\n                    Color.clear\n                        .onChange(of: geo.size.width, initial: true) {\n                            width = geo.size.width\n                        }\n                }\n            }\n    }\n}\n\nextension View {\n    /// Convenience modifier. Attach to a view to load items from the given FeedLoading on appear if the given FeedLoading has no items\n    func widthReader(width: Binding<CGFloat>) -> some View {\n        modifier(WidthReader(width: width))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Utility/Extensions/[BlockNode]+Extensions.swift",
    "content": "//\n//  [BlockNode]+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/08/2024.\n//\n\nimport LemmyMarkdownUI\n\nextension [BlockNode] {\n    // swiftlint:disable:next cyclomatic_complexity function_body_length\n    func rules(isProbableRuleList: Bool = false) -> [[BlockNode]] {\n        var output: [[BlockNode]] = []\n        \n        /// Is `true` when the previous block was a \"Rules\" title, or if there is only one block in the array.\n        var isProbableRuleList: Bool = isProbableRuleList || count == 1\n       \n        /// Stores the parts of a rule currently being parsed.\n        /// This happens when a rule consists of a heading followed by a paragraph.\n        var currentRuleParts: [BlockNode]?\n        \n        // Matches \"1. \", \"2) \", \"Rule 1\" etc\n        let numberedListRegex = /\\d+[-\\.:\\)]\\s+|Rule \\d+[-\\.:\\)]?\\s+/\n        \n        loop: for block in self {\n            if let parts = currentRuleParts {\n                switch block {\n                case .heading:\n                    output.append(parts)\n                    currentRuleParts = nil\n                case .thematicBreak:\n                    if parts.count > 1 {\n                        output.append(parts)\n                        currentRuleParts = nil\n                    } else {\n                        fallthrough\n                    }\n                default:\n                    currentRuleParts?.append(block)\n                    continue loop\n                }\n            }\n            \n            switch block {\n            case let .paragraph(inlines: inlines), .heading(level: _, inlines: let inlines):\n                // Test if the heading is a rule title e.g. \"1. No spam\"\n                if inlines.stringLiteral.starts(with: numberedListRegex) {\n                    // This doesn't preserve the markdown in the title, but it's a rare case for there to be any\n                    let text = String(inlines.stringLiteral.trimmingPrefix(numberedListRegex))\n                    \n                    let blocks: [BlockNode] = [.paragraph(inlines: [.strong(children: [.text(text)])])]\n                    if case .paragraph = block {\n                        output.append(blocks)\n                    } else {\n                        currentRuleParts = blocks\n                    }\n                    break\n                }\n                \n                // AskLemmy uses \"criteria\" rather than \"rules\"\n                // TODO: localize this?\n                if stringIsRulesTitle(inlines.stringLiteral) {\n                    isProbableRuleList = true\n                    continue loop\n                }\n            case .bulletedList(isTight: _, items: let items),\n                 .numberedList(isTight: _, start: _, items: let items):\n                if isProbableRuleList {\n                    output.append(contentsOf: items.map(\\.blocks))\n                }\n                isProbableRuleList = false\n            case let .spoiler(title: title, blocks: blocks):\n                // This handles the situation where a spoiler is used to enclose the rules list.\n                if let title, stringIsRulesTitle(title) {\n                    return blocks.rules(isProbableRuleList: true)\n                }\n                // This handles situations where each item of the rules list contains a spoiler\n                // block that can be expanded for more info on that rule.\n                if let title, title.starts(with: numberedListRegex) {\n                    let text = String(title.trimmingPrefix(numberedListRegex))\n                    let titleBlock: BlockNode = .paragraph(\n                        inlines: [\n                            .strong(children: [.text(text)])\n                        ]\n                    )\n                    output.append([titleBlock] + blocks.filter { $0 != .thematicBreak })\n                }\n            default:\n                break\n            }\n        }\n        \n        if let currentRuleParts {\n            output.append(currentRuleParts)\n        }\n        \n        return output\n    }\n}\n\nprivate func stringIsRulesTitle(_ string: String) -> Bool {\n    [\"Rules\", \"Criteria\"].contains(where: { string.localizedCaseInsensitiveContains($0) })\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Community/AdvancedSortView+SortButton.swift",
    "content": "//\n//  AdvancedSortView+SortButton.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-12.\n//\n\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nextension AdvancedSortView {\n    struct SortButton: View {\n        @Environment(AppState.self) var appState\n        @Environment(HapticManager.self) var hapticManager\n        @Environment(\\.dismiss) var dismiss\n\n        let type: PostSortType\n        var timeRangeFormat: SortTimeRange.FormatStyle = .timescaleFull\n\n        @Binding var selectedSort: PostSortType\n        \n        @State var showingExplanation: Bool = false\n        \n        var body: some View {\n            HStack(spacing: Constants.main.standardSpacing) {\n                Button {\n                    selectedSort = type\n                    dismiss()\n                } label: {\n                    HStack(spacing: Constants.main.standardSpacing) {\n                        Image(icon: type.icon)\n                            .symbolVariant(type == selectedSort ? .fill : .none)\n                            .frame(width: 30, alignment: .center)\n                            .foregroundStyle(type == selectedSort ? .primary : .secondary) // No palette!\n                        titleView\n                            .padding(.vertical, Constants.main.halfSpacing)\n                        Spacer()\n                        Button(\"Pin\", icon: .lemmy.pinned) {\n                            hapticManager.play(haptic: .gentleInfo, tier: .low)\n                            if PinnedSortTracker.main.pinnedSortTypes.contains(type) {\n                                PinnedSortTracker.main.pinnedSortTypes.remove(type)\n                            } else {\n                                PinnedSortTracker.main.pinnedSortTypes.insert(type)\n                            }\n                        }\n                        .symbolVariant(PinnedSortTracker.main.pinnedSortTypes.contains(type) ? .fill : .none)\n                        .labelStyle(.iconOnly)\n                        .foregroundStyle(type == selectedSort ? .themedContrastingLabel : .themedAccent)\n                    }\n                    .frame(minHeight: 45)\n                    .buttonStyle(.plain)\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                    .foregroundStyle(type == selectedSort ? .themedContrastingLabel : .themedPrimary)\n                    .background(\n                        type == selectedSort ? .themedAccent : .themedSecondaryGroupedBackground,\n                        in: .rect(cornerRadius: Constants.main.standardSpacing)\n                    )\n                    .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n                }\n            }\n            .disabled(!appState.firstApi.supports(.postSortType(type), defaultValue: true))\n        }\n        \n        @ViewBuilder\n        var titleView: some View {\n            HStack(spacing: Constants.main.standardSpacing) {\n                Text(type.label(timeRangeFormat: timeRangeFormat))\n                if let explanation = type.explanation {\n                    Button {\n                        showingExplanation.toggle()\n                    } label: {\n                        Image(systemName: \"questionmark.circle\")\n                            .foregroundStyle(.secondary) // No palette!\n                    }\n                    .popover(isPresented: $showingExplanation) {\n                        PopoverContainer {\n                            Text(explanation)\n                                .frame(maxWidth: 200)\n                                .fixedSize(horizontal: false, vertical: true)\n                                .font(.footnote)\n                                .padding(10)\n                                .foregroundStyle(.themedPrimary)\n                        }\n                        .presentationCompactAdaptation(.none)\n                    }\n                    .environment(\\.isEnabled, true) // Janky fix to override the higher-level `.disabled` modifier.\n                }\n            }\n        }\n    }\n}\n\n// https://stackoverflow.com/a/77556014/17629371\nprivate struct PopoverContainer: Layout {\n    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {\n        guard subviews.count == 1 else { fatalError() }\n        let newProposal = ProposedViewSize(\n            width: proposal.width ?? UIScreen.main.bounds.width,\n            height: proposal.height ?? UIScreen.main.bounds.height\n        )\n        return subviews[0].sizeThatFits(newProposal)\n    }\n    \n    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {\n        // entrusts default\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Community/AdvancedSortView.swift",
    "content": "//\n//  AdvancedSortView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-08.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct AdvancedSortView: View {\n    enum Tab: CaseIterable {\n        case sort, filter\n        \n        var label: LocalizedStringResource {\n            switch self {\n            case .sort: \"Sort\"\n            case .filter: \"Filter\"\n            }\n        }\n    }\n    \n    @Environment(AppState.self) var appState\n\n    @State var selectedTab: Tab = .sort\n    @Binding var selectedSort: PostSortType\n    \n    var body: some View {\n        VStack {\n            switch selectedTab {\n            case .sort: sortTab\n            case .filter: filterTab\n            }\n        }\n        .background(.themedGroupedBackground)\n        .presentationBackground(.themedGroupedBackground)\n        .navigationBarTitleDisplayMode(.inline)\n        .toolbar {\n            CloseButtonToolbarItem()\n//            Intentionally left commented out!\n//\n//            ToolbarItem(placement: .principal) {\n//                Picker(\"Tab\", selection: $selectedTab) {\n//                    ForEach(Tab.allCases, id: \\.self) {\n//                        Text($0.label)\n//                    }\n//                }\n//                .frame(maxWidth: .infinity)\n//                .pickerStyle(.segmented)\n//            }\n        }\n    }\n    \n    @ViewBuilder\n    var sortTab: some View {\n        ScrollView {\n            VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n                ForEach(nonTopCases, id: \\.self) { type in\n                    SortButton(type: type, selectedSort: $selectedSort)\n                }\n                subtitle(\"Top of...\")\n                ForEach(topCases, id: \\.self) { type in\n                    SortButton(type: type, selectedSort: $selectedSort)\n                }\n                let unavailableCases = unavailableCases\n                if !unavailableCases.isEmpty {\n                    subtitle(\"Unavailable\")\n                    ForEach(unavailableCases, id: \\.self) { type in\n                        SortButton(type: type, timeRangeFormat: .topAndTimescale, selectedSort: $selectedSort)\n                    }\n                }\n            }\n            .padding(.horizontal, 15)\n        }\n    }\n    \n    @ViewBuilder\n    func subtitle(_ title: LocalizedStringResource) -> some View {\n        Text(title)\n            .foregroundStyle(.secondary)\n            .fontWeight(.semibold)\n            .padding(.leading, Constants.main.standardSpacing)\n            .padding(.top, Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    var filterTab: some View {\n        Text(\"Filter\")\n    }\n    \n    var nonTopCases: [PostSortType] {\n        PostSortType.nonTopCases.filter { appState.firstApi.supports(.postSortType($0), defaultValue: true) }\n    }\n    \n    var topCases: [PostSortType] {\n        PostSortType.legacyTopCases.filter { appState.firstApi.supports(.postSortType($0), defaultValue: true) }\n    }\n    \n    var unavailableCases: [PostSortType] {\n        PostSortType.legacyCases.filter { !appState.firstApi.supports(.postSortType($0), defaultValue: true) }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Community/CommunityAboutView.swift",
    "content": "//\n//  CommunityView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/07/2024.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct CommunityAboutView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.palette) var palette\n\n    let community: Community\n\n    var body: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            if let banner = community.banner {\n                MediaView.largeImage(url: banner, shouldBlur: false)\n            }\n            if let description = community.description {\n                descriptionView(description)\n            } else if canEditDescription {\n                noDescriptionView\n            }\n        }\n        .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n    }\n\n    @ViewBuilder\n    func descriptionView(_ description: String) -> some View {\n        VStack(alignment: .trailing) {\n            if canEditDescription {\n                HStack {\n                    Text(\"Description\")\n                    .font(.callout)\n                    Spacer()\n                    Button(\"Edit\") {\n                        edit()\n                    }\n                    .font(.footnote)\n                    .buttonStyle(.bordered)\n                }\n                .foregroundStyle(.secondary)\n                .fontWeight(.semibold)\n                .padding(.horizontal, Constants.main.standardSpacing)\n                Divider()\n            }\n            Markdown(description, configuration: .default(palette: palette))\n            .padding(.horizontal, Constants.main.standardSpacing)\n        }\n        .padding(.vertical, Constants.main.standardSpacing)\n        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n\n    @ViewBuilder\n    var noDescriptionView: some View {\n        VStack(spacing: 20) {\n            Image(systemName: \"note.text\")\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .frame(width: 50)\n                .foregroundStyle(.tertiary)\n            Button(\"Add description\") {\n                edit()\n            }\n            .buttonStyle(.borderedProminent)\n        }\n        .padding(.vertical, 20)\n    }\n\n    var canEditDescription: Bool {\n        guard appState.firstApi.supports(.editCommunityDescription, defaultValue: false) else { return false }\n        guard let firstPerson = appState.firstPerson else { return false }\n        return (firstPerson.isAdmin.value ?? false) || (firstPerson.moderates?(.community(community)) ?? false)\n    }\n\n    func edit() {\n        navigation.openSheet(.editCommunity(community))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Community/CommunityDetailsView.swift",
    "content": "//\n//  CommunityDetailsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/08/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct CommunityDetailsView: View {\n    let community: Community\n    \n    var body: some View {\n        VStack(spacing: 16) {\n            FormSection {\n                ProfileDateView(profilable: community)\n                    .padding(.vertical, Constants.main.standardSpacing)\n            }\n            \n            FormSection {\n                VStack(spacing: Constants.main.halfSpacing) {\n                    Text(\"Subscribers\")\n                        .foregroundStyle(.themedSecondary)\n                    Text(community.subscription.value?.total ?? 0, format: .number)\n                        .font(.title)\n                        .fontWeight(.semibold)\n                        .contentTransition(.numericText(value: Double(community.subscription.value?.total ?? 0)))\n                        .animation(.default, value: Double(community.subscription.value?.total ?? 0))\n                    \n                    if let localSubscriberCount = community.subscription.value?.local {\n                        Text(localSubscriberCountText)\n                            .contentTransition(.numericText(value: Double(localSubscriberCount)))\n                            .animation(.default, value: Double(localSubscriberCount))\n                            .foregroundStyle(.themedSecondary)\n                            .font(.footnote)\n                    }\n                }\n                .monospacedDigit()\n                .padding(.vertical, Constants.main.standardSpacing)\n            }\n\n            HStack(spacing: 16) {\n                FormReadout(\"Posts\", value: community.postCount.value ?? 0)\n                    .tint(.themedPostAccent)\n                FormReadout(\"Comments\", value: community.commentCount.value ?? 0)\n                    .tint(.themedCommentAccent)\n            }\n            .frame(maxWidth: .infinity)\n            \n            if let activeUserCount = community.activeUserCount.value,\n               community.api.supports(.viewCommunityActiveUsers, defaultValue: true) {\n                ActiveUserCountView(activeUserCount: activeUserCount)\n            }\n        }\n        .padding([.horizontal, .bottom], 16)\n    }\n    \n    var localSubscriberCountText: String {\n        guard let count = community.subscription.value?.local else { return \"\" }\n        return .init(\n            localized: .init(\n                \"local.subscriber.count.text\",\n                defaultValue: \"\\(count) on \\(community.api.host)\",\n                // swiftlint:disable:next line_length\n                comment: \"Used in the \\\"Details\\\" tab of a community page to indicate how many local subscribers use the instance. E.g. \\\"56 on lemmy.world\\\".\"\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Community/CommunitySearchSortPicker.swift",
    "content": "//\n//  CommunitySearchSortPicker.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-12.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CommunitySearchSortPicker: View {\n    @Environment(AppState.self) var appState\n    \n    @Binding var sort: SearchSortType\n    \n    @State var topSortPopupPresented: Bool = false\n\n    var sortTypes: [SearchSortType] {\n        SearchSortType.nonTopCases\n            .filter { appState.firstApi.supports(.searchSortType($0), defaultValue: true) }\n    }\n    \n    var body: some View {\n        Menu(sort.label(timeRangeFormat: .topAndTimescale), icon: sort.icon) {\n            ForEach(sortTypes, id: \\.self) { type in\n                Toggle(\n                    type.label(),\n                    icon: type.icon,\n                    isOn: .init(get: { sort == type }, set: { _ in sort = type })\n                )\n            }\n            Toggle(\n                \"Top...\",\n                icon: .lemmy.topSort,\n                isOn: .init(get: { sort.isTop }, set: { _ in topSortPopupPresented = true })\n            )\n        }\n        .popover(isPresented: $topSortPopupPresented) {\n            TopSortPicker(action: { sort = .top($0) })\n                .presentationBackground(.clear)\n                .presentationCornerRadius(18)\n                .presentationCompactAdaptation(.popover)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Community/CommunityStubResolutionPage.swift",
    "content": "//\n//  CommunityStubResolutionPage.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-02-20.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct CommunityStubResolutionPage: View {\n    @Environment(NavigationLayer.self) var navigation\n    \n    let stub: CommunityStub\n    \n    @State var upgradeError: Error?\n    \n    var body: some View {\n        content\n            .themedGroupedBackground()\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        if let upgradeError {\n            ErrorView(.init(\n                error: upgradeError,\n                refresh: fetchCommunity\n            ))\n        } else {\n            ProgressView()\n                .task {\n                    await fetchCommunity()\n                }\n        }\n    }\n    \n    @discardableResult\n    func fetchCommunity() async -> Bool {\n        do {\n            let community = try await stub.getCommunity()\n            navigation.replace(.community(community, visitContext: .other))\n            return true\n        } catch {\n            upgradeError = error\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Community/CommunityView+Logic.swift",
    "content": "//\n//  CommunityView+Logic.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-01-12.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport QuickSwipes\n\nextension CommunityView {\n    func canEditModeratorList(_ community: Community) -> Bool {\n        guard let firstPerson = appState.firstPerson else { return false }\n        if !firstPerson.api.supports(.editModeratorList, defaultValue: true) {\n            return false\n        }\n        return (firstPerson.isAdmin.value ?? false) || (firstPerson.moderates?(.community(community)) ?? false)\n    }\n\n    func openAddModSheet() {\n        navigation.openSheet(.personPicker { person in\n            newMod = person\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {\n                showingConfirmation = true\n            }\n        })\n    }\n    \n    func addNewMod() {\n        guard let newMod else {\n            assertionFailure(\"newMod cannot be nil\")\n            return\n        }\n\n        Task {\n            do {\n                try await community.addModerator(newMod, added: true)\n            } catch {\n                handleError(error)\n            }\n        }\n    }\n    \n    func moderatorQuickSwipes(community: Community, person: Person) -> SwipeConfiguration {\n        guard let communityModerators = community.moderators.value,\n              canEditModeratorList(community),\n              let myPerson = appState.firstPerson,\n              myPerson.canModerate(person, communityModerators: communityModerators) else {\n            return .init()\n        }\n        \n        return .init(trailingActions: [person.addModAction(community: community, isOn: true)])\n    }\n    \n    func setupFeedLoader(community: Community) {\n        if postFeedLoader == nil {\n            Task { @MainActor in\n                @Setting(\\.behavior_internetSpeed) var internetSpeed\n                @Setting(\\.feed_showRead) var showReadInFeed\n                postFeedLoader = try await .init(\n                    pageSize: internetSpeed.pageSize,\n                    sortType: appState.initialFeedSortType,\n                    showReadPosts: showReadInFeed,\n                    filterContext: filtersTracker.filterContext,\n                    prefetchingConfiguration: .forPostSize(postSize),\n                    urlCache: Constants.main.urlCache,\n                    community: community\n                )\n            }\n        } else if postFeedLoader?.community.api != community.api {\n            postFeedLoader?.community = community\n        }\n    }\n    \n    func logVisit(_ community: Community) {\n        if let session = (appState.firstSession as? UserSession), let visitHistory = session.visitHistory {\n            guard session.api === community.api else { return }\n            visitHistory.addCommunity(community, context: visitContext)\n            Task(priority: .background) {\n                try await session.saveVisitHistory()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Community/CommunityView.swift",
    "content": "//\n//  CommunityView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/07/2024.\n//\n\nimport Actions\nimport Dependencies\nimport Haptics\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct CommunityView: View {\n    enum Tab: String, CaseIterable, Identifiable {\n        case posts, comments, about, moderation, details\n\n        var id: Self { self }\n        var label: LocalizedStringResource {\n            switch self {\n            case .posts: \"Posts\"\n            case .comments: \"Comments\"\n            case .about: \"About\"\n            case .moderation: \"Moderation\"\n            case .details: \"Details\"\n            }\n        }\n    }\n        \n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(FiltersTracker.self) var filtersTracker\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.palette) var palette\n    @Environment(\\.dismiss) var dismiss\n    \n    @Setting(\\.post_size) var postSize\n    @Setting(\\.feed_showRead) var showRead\n    @Setting(\\.safety_enableNsfwCommunityWarning) var showNsfwCommunityWarning\n    @Setting(\\.safety_blurNsfw) var blurNsfw\n    \n    @ObservationIgnored @Dependency(\\.persistenceRepository) private var persistenceRepository\n    \n    let visitContext: VisitHistory.VisitContext\n\n    @State var community: Community\n    @State private var selectedTab: Tab = .posts\n    @State var postFeedLoader: CommunityPostFeedLoader?\n    @State var warningPresented: Bool\n    @State var isLoading: Bool = false\n    \n    @State var showingConfirmation: Bool = false\n    @State var newMod: Person?\n    @State var showHiddenReadBanner: Bool = false\n    @State var lastRefreshDate: Date?\n    \n    init(\n        community: Community,\n        visitContext: VisitHistory.VisitContext\n    ) {\n        @Setting(\\.safety_enableNsfwCommunityWarning) var showNsfwCommunityWarning\n        self.community = community\n        self.visitContext = visitContext\n        self._warningPresented = .init(wrappedValue: showNsfwCommunityWarning && community.nsfw)\n    }\n    \n    var body: some View {\n        content\n            .reloadOnAccountSwitch(entity: $community, isLoading: $isLoading)\n            .externalApiWarning(entity: community, isLoading: isLoading)\n            .task {\n                setupFeedLoader(community: community)\n            }\n            .onAppear {\n                logVisit(community)\n            }\n            .navigationBarTitleDisplayMode(.inline)\n            .frame(maxWidth: .infinity, maxHeight: .infinity)\n            .themedGroupedBackground()\n            .environment(\\.communityContext, community)\n            .environment(\\.feedContext, .community)\n    }\n        \n    @ViewBuilder\n    var content: some View {\n        FancyScrollView {\n            HStack {\n                FeedHeaderView(\n                    title: Text(community.displayName),\n                    subtitle: Text(community.fullNameWithPrefix),\n                    dropdownStyle: .disabled,\n                    image: {\n                        CircleCroppedImageView(\n                            community,\n                            frame: Constants.main.feedHeaderSize,\n                            blurred: community.nsfw && (blurNsfw == .always)\n                        )\n                    }\n                )\n                subscribeButton\n                    .padding(.top, Constants.main.halfSpacing)\n            }\n            BubblePicker(\n                tabs,\n                selected: $selectedTab,\n                label: \\.label\n            )\n            VStack {\n                switch selectedTab {\n                case .posts:\n                    VStack {\n                        if let postFeedLoader {\n                            postsTab(postFeedLoader: postFeedLoader)\n                                .padding(.bottom, -4)\n                        }\n                    }\n                    .toolbar {\n                        ToolbarItem(placement: .topBarTrailing) {\n                            FeedSortPicker(feedLoader: postFeedLoader, showTopTimescaleInIcon: true)\n                        }\n                    }\n                case .about:\n                    CommunityAboutView(community: community)\n                case .moderation:\n                    moderationTab\n                case .details:\n                    CommunityDetailsView(community: community)\n                default:\n                    EmptyView()\n                }\n            }\n        }\n        .animation(.snappy, value: showHiddenReadBanner && !showRead)\n        .conditionalNavigationTitle(community.name)\n        .toolbar {\n            ToolbarItemGroup(placement: .secondaryAction) {\n                ActionButtons(community: community)\n                    .environment(postFeedLoader)\n            }\n        }\n        // don't show the refresh popup if community api isn't the active api, since that indicates an unresolvable community\n        .popupAnchor()\n        .outdatedFeedPopup(\n            feedLoader: postFeedLoader,\n            showPopup: selectedTab == .posts && community.api === appState.firstApi,\n            onManualRefresh: {\n                guard !showRead else { return }\n                let now = Date()\n                if let lastRefresh = lastRefreshDate,\n                   now.timeIntervalSince(lastRefresh) < 5 {\n                    showHiddenReadBanner = true\n                }\n                lastRefreshDate = now\n            }\n        )\n        .onChange(of: showRead) {\n            if showRead {\n                showHiddenReadBanner = false\n            }\n            lastRefreshDate = nil\n        }\n        .fullScreenCover(isPresented: $warningPresented) {\n            WarningOverlayView(\n                text: \"This community likely contains graphic or explicit content.\",\n                isPresented: $warningPresented,\n                showWarningAgain: $showNsfwCommunityWarning\n            )\n        }\n    }\n    \n    @ViewBuilder\n    func postsTab(postFeedLoader: CommunityPostFeedLoader) -> some View {\n        if community.removed {\n            VStack(spacing: Constants.main.standardSpacing) {\n                Image(icon: .lemmy.remove)\n                    .font(.title)\n                Text(\"This community has been removed.\")\n                    .fontWeight(.semibold)\n            }\n            .foregroundStyle(.themedWarning)\n            .padding(.top, Constants.main.doubleSpacing)\n        } else {\n            if showHiddenReadBanner, !showRead {\n                HiddenReadBannerView {\n                    showHiddenReadBanner = false\n                }\n                .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n            }\n            PostGridView(postFeedLoader: postFeedLoader)\n        }\n    }\n\n    @ViewBuilder\n    var moderationTab: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            if community.api.supports(.modlog, defaultValue: true) {\n                ModlogButtonView(community: community)\n            }\n\n            VStack(spacing: Constants.main.halfSpacing) {\n                // ExpectedView causes rendering issues here\n                ForEach(community.moderators.value ?? []) { person in\n                    PersonListRow(person)\n                        .quickSwipes(moderatorQuickSwipes(community: community, person: person))\n                }\n            }\n            \n            if canEditModeratorList(community) {\n                Button(\"Add Moderator\", icon: .general.add, action: openAddModSheet)\n                    .buttonStyle(.capsule)\n                    .confirmationDialog(\"Add Moderator\", isPresented: $showingConfirmation) {\n                        Button(\"Yes\", action: addNewMod)\n                    } message: {\n                        if let displayName = newMod?.displayName {\n                            Text(\"Really appoint \\(displayName) as a moderator of \\(community.displayName)?\")\n                        } else {\n                            Text(\"Really appoint this user as a moderator of \\(community.displayName)?\")\n                        }\n                    }\n            }\n        }\n        .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    var subscribeButton: some View {\n        if let subscription = community.subscription.value,\n           let updateSubscribed = community.updateSubscribed {\n            Button {\n                if community.api.willSendToken {\n                    hapticManager.play(haptic: .gentleInfo, tier: .low)\n                    updateSubscribed(!subscription.subscribed)\n                }\n            } label: {\n                HStack {\n                    Text(subscription.total.abbreviated)\n                    Image(icon: subscription.subscribed ? .general.success : .lemmy.personAvatar)\n                        .symbolVariant(.circle)\n                        .symbolVariant(subscription.subscribed ? .fill : .none)\n                        .symbolRenderingMode(.hierarchical)\n                }\n                .fontWeight(.semibold)\n                .padding(.vertical, 3)\n                .padding(.trailing, 6)\n                .padding(.leading, 8)\n                .background(subscription.subscribed ? .themedAccent : .themedSecondary.opacity(0.2), in: .capsule)\n                .foregroundStyle(subscription.subscribed ? .themedContrastingLabel : .themedSecondary)\n            }\n            .padding(.trailing, Constants.main.standardSpacing)\n            .padding(.bottom, Constants.main.halfSpacing)\n        }\n    }\n    \n    var tabs: [Tab] {\n        var output: [Tab] = [.posts, .moderation, .details]\n        let canModerate: Bool\n        if !appState.firstApi.supports(.editCommunityDescription, defaultValue: false) {\n            canModerate = false\n        } else if let firstPerson = appState.firstPerson {\n            canModerate = (firstPerson.moderates?(.community(community)) ?? false) || (firstPerson.isAdmin.value ?? false)\n        } else {\n            canModerate = false\n        }\n        if community.description != nil || community.banner != nil || canModerate {\n            output.insert(.about, at: 1)\n        }\n        return output\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment(api: .realistic)) {\n//        CommunityView(\n//            community: .init(Community2.mock(.realistic(.pics), api: .realistic)),\n//            visitContext: .other\n//        )\n//        .previewNavigationStack()\n//        .previewTabBar(selected: .feeds)\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Community/FeedSortPicker.swift",
    "content": "//\n//  FeedSortPicker.swift\n//  Mlem\n//\n//  Created by Sjmarf on 10/08/2024.\n//\n\nimport Flow\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct FeedSortPicker: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    enum Value: Hashable {\n        case known(PostSortType)\n        case unknown\n        \n        var sortType: PostSortType? {\n            switch self {\n            case let .known(postSortType): postSortType\n            case .unknown: nil\n            }\n        }\n    }\n    \n    let showTopTimescaleInIcon: Bool\n    @Binding var value: Value\n    \n    init(sort: Binding<PostSortType>, showTopTimescaleInIcon: Bool = false) {\n        self._value = .init(get: { .known(sort.wrappedValue) }, set: {\n            if let sortType = $0.sortType {\n                sort.wrappedValue = sortType\n            } else {\n                assertionFailure()\n            }\n        })\n        self.showTopTimescaleInIcon = showTopTimescaleInIcon\n    }\n    \n    init(feedLoader: CommunityPostFeedLoader?, showTopTimescaleInIcon: Bool = false) {\n        self._value = .init(get: {\n            if let feedLoader {\n                .known(feedLoader.sortType)\n            } else {\n                .unknown\n            }\n        }, set: { value in\n            if let sort = value.sortType {\n                Task { @MainActor in\n                    do {\n                        try await feedLoader?.changeSortType(to: sort, forceRefresh: false)\n                    } catch {\n                        handleError(error)\n                    }\n                }\n            }\n        })\n        self.showTopTimescaleInIcon = showTopTimescaleInIcon\n    }\n\n    init(feedLoader: AggregatePostFeedLoader?, showTopTimescaleInIcon: Bool = false) {\n        self._value = .init(get: {\n            if let feedLoader {\n                .known(feedLoader.sortType)\n            } else {\n                .unknown\n            }\n        }, set: { value in\n            if let sort = value.sortType {\n                Task { @MainActor in\n                    do {\n                        try await feedLoader?.changeSortType(to: sort, forceRefresh: false)\n                    } catch {\n                        handleError(error)\n                    }\n                }\n            }\n        })\n        self.showTopTimescaleInIcon = showTopTimescaleInIcon\n    }\n\n    var nonTopSortTypes: [PostSortType] {\n        PostSortType.nonTopCases\n            .filter { PinnedSortTracker.main.pinnedSortTypes.contains($0) }\n            .filter { appState.firstApi.supports(.postSortType($0), defaultValue: true) }\n    }\n    \n    var topSortTypes: [PostSortType] {\n        PostSortType.legacyTopCases\n            .filter { PinnedSortTracker.main.pinnedSortTypes.contains($0) }\n            .filter { appState.firstApi.supports(.postSortType($0), defaultValue: true) }\n    }\n    \n    var body: some View {\n        Menu {\n            Section {\n                ForEach(nonTopSortTypes, id: \\.self) { type in\n                    Toggle(\n                        type.label(),\n                        icon: type.icon,\n                        isOn: .init(get: { value.sortType == type }, set: { _ in value = .known(type) })\n                    )\n                }\n                if collapseTopSorts {\n                    Menu(\"Top...\", icon: .lemmy.topSort) {\n                        topResultsView\n                    }\n                }\n            }\n            if !collapseTopSorts {\n                Section(\"Top...\") {\n                    topResultsView\n                }\n            }\n            Section {\n                Button(\"More...\", icon: .general.toolbarMenu) {\n                    navigation.openSheet(.advancedSorting(.init(get: {\n                        value.sortType ?? .hot\n                    }, set: {\n                        value = .known($0)\n                    })))\n                }\n            }\n        } label: {\n            labelView\n        }\n        .disabled(!appState.firstApi.contextIsFetched)\n    }\n    \n    var collapseTopSorts: Bool {\n        topSortTypes.count > 3 && !nonTopSortTypes.isEmpty\n    }\n\n    @ViewBuilder\n    var topResultsView: some View {\n        ForEach(topSortTypes, id: \\.self) { type in\n            Toggle(\n                type.label(timeRangeFormat: .timescaleFull),\n                icon: type.icon,\n                isOn: .init(get: { value.sortType == type }, set: { _ in\n                    value = .known(type)\n                })\n            )\n        }\n    }\n\n    @ViewBuilder\n    var labelView: some View {\n        VStack {\n            if showTopTimescaleInIcon, let sort = value.sortType, sort.isTop {\n                HStack {\n                    Image(icon: .lemmy.topSort)\n                        .imageScale(.small)\n                    Text(sort.label(timeRangeFormat: .timescaleAbbreviated))\n                        .font(UIDevice.isIos26 ? .body : .footnote)\n                        .fontDesign(.rounded)\n                }\n                .padding(.horizontal, 10)\n                .padding(.vertical, 5)\n                .background {\n                    if !UIDevice.isIos26 {\n                        Capsule()\n                            // 1.51 is intentional - iOS doesn't render it quite right at 1.5 (iPhone 12)\n                            .strokeBorder(.themedAccent, lineWidth: 1.51)\n                    }\n                }\n                .accessibilityLabel(sort.label(timeRangeFormat: .topAndTimescale))\n            } else if let sortType = value.sortType {\n                Label(sortType.label(timeRangeFormat: topSortTypes.count == 1 ? .topOnly : .topAndTimescale), icon: sortType.icon)\n            }\n        }\n        .animation(.easeOut(duration: 0.4), value: value == .unknown)\n    }\n    \n    var formatter: DateComponentsFormatter {\n        let formatter = DateComponentsFormatter()\n        formatter.unitsStyle = .abbreviated\n        formatter.maximumUnitCount = 1\n        return formatter\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Community/TopSortPicker.swift",
    "content": "//\n//  FeedSortPicker+TopSortPicker.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-12.\n//\n\nimport Flow\nimport MlemMiddleware\nimport SwiftUI\n\nstruct TopSortPicker: View {\n    @Environment(AppState.self) var appState\n    @Environment(\\.dismiss) var dismiss\n    @Environment(\\.colorScheme) var colorScheme\n    \n    var action: (SortTimeRange) -> Void\n    var filter: (SortTimeRange) -> Bool = { _ in true }\n    \n    var timeRanges: [SortTimeRange] {\n        SortTimeRange.legacyCases\n            .filter(filter)\n            .filter { appState.firstApi.supports(.sortTimeRange($0), defaultValue: true) }\n    }\n    \n    var body: some View {\n        HFlow(spacing: 10) {\n            ForEach(timeRanges, id: \\.self) { type in\n                button(type)\n                    .frame(minWidth: 60)\n            }\n        }\n        .padding(10)\n        .frame(width: 222)\n    }\n    \n    @ViewBuilder\n    func button(_ type: SortTimeRange) -> some View {\n        Button {\n            action(type)\n            dismiss()\n        } label: {\n            Group {\n                if type == .allTime {\n                    if timeRanges.count % 3 == 0 {\n                        Text(\"All\")\n                    } else {\n                        Text(\"All Time\")\n                    }\n                } else {\n                    Text(type.label(name: \"Top\", prefix: \"Top:\", format: .timescaleAbbreviated))\n                }\n            }\n            .frame(maxWidth: .infinity)\n            .contentShape(.rect)\n        }\n        .frame(maxWidth: .infinity)\n        .frame(height: 40)\n        .background {\n            if colorScheme == .dark {\n                RoundedRectangle(cornerRadius: 8)\n                    .fill(.themedPrimary.opacity(0.2))\n            } else {\n                RoundedRectangle(cornerRadius: 8)\n                    .fill(.themedBackground)\n                    .shadow(color: .black.opacity(0.05), radius: 3)\n            }\n        }\n        .foregroundStyle(.themedPrimary)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/DeleteAccountView.swift",
    "content": "//\n//  DeleteAccountView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-19.\n//\n\nimport ComponentViews\nimport Dependencies\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nstruct DeleteAccountView: View {\n    @Environment(AppState.self) var appState\n    @Environment(\\.dismiss) var dismiss\n    \n    let account: UserAccount\n    \n    @State private var password = \"\"\n    @State var confirmed: Bool = false\n    @State var deleteContent: Bool = true\n    \n    var body: some View {\n        content\n            .task {\n                do {\n                    try await account.api.ensureContextPresence()\n                } catch {\n                    handleError(error)\n                }\n            }\n    }\n    \n    var content: some View {\n        VStack(alignment: .center, spacing: Constants.main.doubleSpacing) {\n            Text(\"Really delete \\(account.name)?\")\n                .font(.title)\n                .fontWeight(.bold)\n            \n            WarningView(\n                icon: .general.warning,\n                text: \"This will permanently remove it from \\(account.host), not just Mlem!\",\n                inList: false\n            )\n            \n            deleteConfirmation\n            \n            CloseButtonView(ios18Label: .cancel)\n        }\n        .multilineTextAlignment(.center)\n        .padding(Constants.main.doubleSpacing)\n    }\n    \n    @ViewBuilder\n    var deleteConfirmation: some View {\n        if confirmed {\n            if account.api.contextIsFetched {\n                passwordPrompt()\n            } else {\n                VStack(spacing: Constants.main.standardSpacing) {\n                    ProgressView()\n                    Text(\"Loading instance details\")\n                        .foregroundStyle(.themedSecondary)\n                }\n            }\n        } else {\n            Button(\"Permanently delete \\(account.name)\") {\n                withAnimation {\n                    confirmed = true\n                }\n            }\n            .buttonStyle(.borderedProminent)\n            .tint(.red)\n        }\n    }\n    \n    @ViewBuilder\n    func passwordPrompt() -> some View {\n        Text(\"To confirm, please enter your password:\")\n        \n        Group {\n            SecureField(String(\"\"), text: $password)\n                .padding(4)\n                .background(.themedSecondaryBackground)\n                .cornerRadius(Constants.main.smallItemCornerRadius)\n                .textContentType(.password)\n                .submitLabel(.go)\n                .onSubmit {\n                    deleteAccount()\n                }\n                .labelsHidden()\n            \n            Toggle(isOn: $deleteContent) {\n                Text(\"Delete posts and comments\")\n            }\n            \n            Button(\"Permanently delete \\(account.name)\") {\n                deleteAccount()\n            }\n            .buttonStyle(.borderedProminent)\n            .tint(.red)\n        }\n        .padding(.horizontal, 30)\n    }\n    \n    func deleteAccount() {\n        Task {\n            do {\n                try await account.api.deleteAccount(password: password, deleteContent: deleteContent)\n                Task { @MainActor in\n                    AccountsTracker.main.removeAccount(account: account)\n                    dismiss()\n                }\n            } catch {\n                handleError(error)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/CommentEditorView+Context.swift",
    "content": "//\n//  CommentEditorView+Context.swift\n//  Mlem\n//\n//  Created by Sjmarf on 20/08/2024.\n//\n\nimport MlemMiddleware\n\nextension CommentEditorView {\n    enum Context: Hashable {\n        case post(Post)\n        case comment(Comment)\n        \n        static func == (lhs: Context, rhs: Context) -> Bool {\n            lhs.hashValue == rhs.hashValue\n        }\n        \n        func hash(into hasher: inout Hasher) {\n            switch self {\n            case let .post(post):\n                hasher.combine(\"post\")\n                hasher.combine(post.hashValue)\n            case let .comment(comment):\n                hasher.combine(\"comment\")\n                hasher.combine(comment.hashValue)\n            }\n        }\n        \n        var item: any SelectableContentProviding {\n            switch self {\n            case let .post(post): post\n            case let .comment(comment): comment\n            }\n        }\n        \n        var api: ApiClient {\n            switch self {\n            case let .post(post):\n                post.api\n            case let .comment(comment):\n                comment.api\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/CommentEditorView+Logic.swift",
    "content": "//\n//  CommentEditorView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 20/08/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension CommentEditorView {\n    func resolveContext() async {\n        guard let originalContext else { return }\n        do {\n            if originalContext.api === account.api {\n                resolutionState = .success\n                resolvedContext = originalContext\n            } else {\n                Task { @MainActor in\n                    resolutionState = .resolving\n                }\n                switch originalContext {\n                case let .post(post):\n                    let post = try await account.api.getPost(url: post.actorId.url)\n                    Task { @MainActor in\n                        resolutionState = .success\n                        resolvedContext = .post(post)\n                    }\n                case let .comment(comment):\n                    let comment = try await account.api.getComment(url: comment.actorId.url)\n                    Task { @MainActor in\n                        resolutionState = .success\n                        resolvedContext = .comment(comment)\n                    }\n                }\n            }\n            \n        } catch ApiClientError.noEntityFound {\n            handleError(ApiClientError.noEntityFound, silent: true)\n            Task { @MainActor in\n                resolutionState = .notFound\n            }\n        } catch {\n            Task { @MainActor in\n                resolutionState = .error(.init(error: error))\n            }\n        }\n    }\n    \n    func inferContextFromCommentToEdit() async {\n        guard originalContext == nil else { return }\n        do {\n            if let commentToEdit {\n                if let parent = try await commentToEdit.getParent(cachedValueAcceptable: true) {\n                    originalContext = .comment(parent)\n                } else if let post = commentToEdit.post.value_ {\n                    originalContext = .post(post)\n                }\n            }\n        } catch {\n            handleError(error)\n        }\n    }\n    \n    func send() async {\n        uploadHistory.deleteWhereNotPresent(in: textView.text)\n        do {\n            if let commentToEdit {\n                try await commentToEdit.edit(content: textView.text, languageId: nil)\n            } else if let resolvedContext {\n                let result: Comment\n                let parent: Comment?\n                switch resolvedContext {\n                case let .post(post):\n                    result = try await post.reply(content: textView.text, languageId: nil)\n                    parent = nil\n                case let .comment(comment):\n                    result = try await comment.reply(content: textView.text)\n                    parent = comment\n                }\n                commentTreeTracker?.insertCreatedComment(result, parent: parent)\n            } else {\n                assertionFailure()\n                return\n            }\n            Task { @MainActor in\n                textView.resignFirstResponder()\n                textView.isEditable = false\n                hapticManager.play(haptic: .success, tier: .low)\n                dismiss()\n            }\n        } catch {\n            Task { @MainActor in\n                sending = false\n                textView.isEditable = true\n                handleError(error)\n            }\n        }\n    }\n    \n    func checkSlurFilter(text: String) {\n        do {\n            if let output = try slurRegex?.firstMatch(in: text.lowercased()) {\n                slurMatch = String(text[output.range])\n            } else {\n                slurMatch = nil\n            }\n        } catch {\n            handleError(error, silent: true)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/CommentEditorView.swift",
    "content": "//\n//  CommentEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 14/07/2024.\n//\n\nimport ComponentViews\nimport Haptics\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport os\nimport SwiftUI\nimport Theming\n\nstruct CommentEditorView: View {\n    private let log: Logger = .mlemLogger()\n    \n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.dismiss) var dismiss\n    \n    @Setting(\\.person_showAvatar) private var showPersonAvatar\n    @Setting(\\.community_showAvatar) private var showCommunityAvatar\n    \n    enum ResolutionState: Equatable {\n        case success, notFound, error(ErrorDetails), resolving\n    }\n    \n    @State var textView: UITextView = .init()\n\n    let commentTreeTracker: CommentTreeTracker?\n    \n    @State var commentToEdit: Comment?\n    @State var originalContext: Context?\n    @State var resolvedContext: Context?\n    @State var resolutionState: ResolutionState = .success\n    @State var sending: Bool = false\n\n    @State var account: UserAccount\n    @State var presentationSelection: PresentationDetent = .large\n    \n    @State var textIsEmpty: Bool = true\n    @State var markdownToolbarEditorModel: MarkdownEditorToolbarModel = .init()\n    @State var uploadHistory: ImageUploadHistoryManager = .init()\n    @State var slurMatch: String?\n    \n    @State var slurRegex: Regex<AnyRegexOutput>?\n    \n    init?(\n        commentToEdit: Comment? = nil,\n        context: Context? = nil,\n        commentTreeTracker: CommentTreeTracker? = nil\n    ) {\n        self.commentToEdit = commentToEdit\n        self._originalContext = .init(wrappedValue: context)\n        self._resolvedContext = .init(wrappedValue: context)\n        self.commentTreeTracker = commentTreeTracker\n        if let userAccount = (AppState.main.firstAccount as? UserAccount) {\n            self._account = .init(wrappedValue: userAccount)\n        } else {\n            return nil\n        }\n        self._slurRegex = .init(wrappedValue: AppState.main.firstApi.myInstance?.slurRegex())\n        \n        textView.text = commentToEdit?.content ?? \"\"\n    }\n        \n    var minTextEditorHeight: CGFloat {\n        UIFont.preferredFont(forTextStyle: .body).lineHeight * 4 + 15\n    }\n\n    var body: some View {\n        CollapsibleSheetView(presentationSelection: $presentationSelection, canDismiss: textIsEmpty) {\n            NavigationStack {\n                content\n                    .navigationBarTitleDisplayMode(.inline)\n                    .toolbar {\n                        ToolbarItem(placement: .topBarLeading) {\n                            CloseButtonView(ios18Label: .cancel, requiresConfirmation: !textIsEmpty)\n                        }\n                        ToolbarItem(placement: .principal) {\n                            if AccountsTracker.main.userAccounts.count > 1, commentToEdit == nil {\n                                AccountPickerMenu(account: $account) {\n                                    HStack(spacing: 3) {\n                                        FullyQualifiedLabelView(account, labelStyle: .medium, showAvatar: false)\n                                        Image(icon: .general.dropDown)\n                                            .symbolVariant(.circle.fill)\n                                            .symbolRenderingMode(.hierarchical)\n                                            .tint(.themedSecondary)\n                                            .imageScale(.small)\n                                            .fontWeight(.bold)\n                                    }\n                                }\n                            }\n                        }\n                        ToolbarItem(placement: .topBarTrailing) {\n                            if sending {\n                                ProgressView()\n                            } else {\n                                sendButton\n                            }\n                        }\n                    }\n                    .background(.themedGroupedBackground)\n                    .presentationBackground(.themedGroupedBackground)\n            }\n            .task(id: account) { await resolveContext() }\n        }\n        .onDisappear {\n            // If we didn't have the `isAlive` check here, the images would\n            // get deleted when you click on a link in the reply context\n            if !navigation.isAlive, !sending, !uploadHistory.uploads.isEmpty {\n                log.debug(\"Deleting uploaded images...\")\n                uploadHistory.deleteAll()\n            }\n        }\n        .onChange(of: presentationSelection) {\n            if presentationSelection == .large {\n                textView.becomeFirstResponder()\n            }\n        }\n        .onChange(of: navigation.isTopSheet) {\n            if navigation.isTopSheet, navigation.model != nil {\n                textView.becomeFirstResponder()\n            }\n        }\n        .onChange(of: account) {\n            if let instance = account.api.myInstance {\n                slurRegex = instance.slurRegex()\n                checkSlurFilter(text: textView.text)\n            } else {\n                Task {\n                    do {\n                        let instance = try await account.api.getMyInstance()\n                        slurRegex = instance.slurRegex()\n                        checkSlurFilter(text: textView.text)\n                    } catch {\n                        handleError(error)\n                    }\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        ScrollView {\n            VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n                if resolutionState == .notFound {\n                    resolutionWarning\n                        .padding(.horizontal, 10)\n                }\n                \n                VStack(spacing: Constants.main.standardSpacing) {\n                    MarkdownTextEditor(\n                        onChange: {\n                            if $0.isEmpty != textIsEmpty {\n                                textIsEmpty = $0.isEmpty\n                            }\n                            checkSlurFilter(text: $0)\n                        },\n                        prompt: \"Start writing...\",\n                        textView: textView,\n                        content: {\n                            MarkdownEditorToolbarView(\n                                textView: textView,\n                                uploadHistory: uploadHistory,\n                                model: markdownToolbarEditorModel\n                            )\n                        }\n                    )\n                    .onChange(of: account.api, initial: true) {\n                        markdownToolbarEditorModel.imageUploadApi = account.api\n                    }\n                    \n                    if let slurMatch {\n                        FilterViolationWarning(failures: [account.host: slurMatch])\n                            .padding(.horizontal, Constants.main.standardSpacing)\n                    }\n                }\n                .frame(\n                    maxWidth: .infinity,\n                    minHeight: minTextEditorHeight,\n                    maxHeight: .infinity,\n                    alignment: .topLeading\n                )\n                .padding(.vertical, Constants.main.standardSpacing)\n                .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n                .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n                .padding(.horizontal, Constants.main.standardSpacing)\n                \n                contextView\n                    .padding(Constants.main.standardSpacing)\n                    .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n                    .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n                    .padding(.horizontal, Constants.main.standardSpacing)\n            }\n            .animation(.easeOut(duration: 0.2), value: resolutionState == .notFound)\n            .padding(.bottom, Constants.main.standardSpacing)\n        }\n        .scrollBounceBehavior(.basedOnSize)\n        .task { await inferContextFromCommentToEdit() }\n    }\n    \n    @ViewBuilder\n    var contextView: some View {\n        switch originalContext {\n        case let .post(post):\n            VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n                HStack {\n                    ExpectedView(post.community) { community in\n                        FullyQualifiedLinkView(\n                            community,\n                            labelStyle: .medium,\n                            blurred: post.nsfw\n                        )\n                    } placeholder: {\n                        Text(verbatim: .communityPlaceholder).redacted(reason: .placeholder)\n                    }\n                    Spacer()\n                    selectTextButton\n                }\n                LargePostBodyView(post: post, isPostPage: true, shouldBlur: false)\n                ExpectedView(post.creator) { creator in\n                    FullyQualifiedLinkView(\n                        creator,\n                        labelStyle: .medium,\n                        blurred: post.nsfw)\n                } placeholder: {\n                    Text(verbatim: .personPlaceholder).redacted(reason: .placeholder)\n                }\n            }\n        case let .comment(comment):\n            VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n                HStack {\n                    ExpectedView(comment.creator) { creator in\n                        FullyQualifiedLinkView(\n                            creator,\n                            labelStyle: .small\n                        )\n                    } placeholder: {\n                        Text(verbatim: .personPlaceholder).redacted(reason: .placeholder)\n                    }\n                    Spacer()\n                    selectTextButton\n                }\n                CommentBodyView(comment: comment)\n            }\n        case nil:\n            ProgressView()\n        }\n    }\n    \n    @ViewBuilder\n    var selectTextButton: some View {\n        Button(\"Select Text\", icon: .general.select) {\n            Task { @MainActor in\n                textView.resignFirstResponder()\n            }\n            originalContext?.item.showTextSelectionSheet()\n        }\n        .labelStyle(.iconOnly)\n    }\n    \n    @ViewBuilder\n    var resolutionWarning: some View {\n        Text(\"Failed to resolve post. Try another account.\")\n            .padding(.vertical, 3)\n            .frame(maxWidth: .infinity)\n            .background(.opacity(0.2), in: .capsule)\n            .foregroundStyle(.themedCaution)\n    }\n    \n    @ViewBuilder\n    var sendButton: some View {\n        Button(\"Send\", icon: commentToEdit != nil ? .general.success : .lemmy.send) {\n            sending = true\n            Task(priority: .userInitiated) {\n                await send()\n            }\n        }\n        .disabled(resolutionState != .success || textIsEmpty || slurMatch != nil)\n        .glassProminentButtonStyle()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/PostEditor/LinkEditorView.swift",
    "content": "//\n//  LinkEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-11-04.\n//  \n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct LinkEditorView: View {\n    @Environment(\\.palette) var palette\n    let api: ApiClient\n    let close: (PostLink) -> Void\n    \n    let originalUrl: URL\n    \n    init(url: URL, api: ApiClient, close: @escaping (PostLink) -> Void) {\n        self.api = api\n        self.close = close\n        self.originalUrl = url\n    }\n    \n    @State var urlString: String = \"\"\n    @State var textView: UITextView?\n    @State var isSubmitting: Bool = false\n    @FocusState var focused: Bool\n\n    var attributedStringBinding: Binding<AttributedString> {\n        .init {\n            var string = AttributedString(urlString)\n            string.foregroundColor = ThemedColor.themedSecondary.resolve(with: palette)\n            if let url = URL(string: urlString), let host = url.host() {\n                if let range = string.range(of: host) {\n                    string[range].foregroundColor = ThemedColor.themedPrimary.resolve(with: palette)\n                }\n            }\n            return string\n        } set: { \n            urlString = String($0.characters)\n        }\n    }\n\n    var body: some View {\n        VStack(spacing: 5) {\n            HStack {\n                Spacer()\n                Button {\n                    if let url = URL(string: self.urlString) {\n                        focused = false\n                        Task {\n                            self.isSubmitting = true\n                            let link: PostLink\n                            do {\n                                link = try await api.getPostLinkOrUseOpenGraph(url: url)\n                            } catch {\n                                link = .init(content: url, thumbnail: nil, label: url.absoluteString)\n                                handleError(error, silent: true)\n                            }\n                            self.isSubmitting = false\n                            close(link)\n                        }\n                    }\n                } label: {\n                    Label(\"Done\", icon: .general.success)\n                        .font(.title)\n                        .fontWeight(.semibold)\n                        .imageScale(.large)\n                        .labelStyle(.iconOnly)\n                        .symbolVariant(.circle.fill)\n                        .symbolRenderingMode(.palette)\n                        .foregroundStyle(.secondary, .themedTertiaryGroupedBackground)\n                        .opacity(isSubmitting ? 0 : 1)\n                        .overlay {\n                            if isSubmitting {\n                                ProgressView()\n                            }\n                        }\n                }\n                .buttonStyle(.plain)\n            }\n            textEditor\n                .padding(.horizontal, 5)\n        }\n        .padding(Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    var textEditor: some View {\n        Group {\n            if #available(iOS 26.0, *) {\n                TextEditor(text: attributedStringBinding)\n            } else {\n                TextEditor(text: $urlString)\n            }\n        }\n        .focused($focused)\n        .onAppear {\n            focused = true\n        }\n        .fixedSize(horizontal: false, vertical: true)\n        .layoutPriority(6)\n        .frame(maxHeight: .infinity)\n        .scrollContentBackground(.hidden)\n        .introspect(.textEditor, on: .iOS(.v26)) {\n            if textView == nil {\n                textView = $0\n                // The text has to be set here; otherwise the textview has a height of 0 for some reason\n                textView?.text = originalUrl.absoluteString\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/PostEditor/PostEditorTargetView.swift",
    "content": "//\n//  PostEditorTargetView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 14/08/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct PostEditorTargetView: View {\n    @Environment(NavigationLayer.self) private var navigation\n    \n    @Bindable var target: PostEditorTarget\n    let isMoreThanOneTarget: Bool\n    \n    var body: some View {\n        HStack {\n            HStack(spacing: Constants.main.standardSpacing) {\n                if AccountsTracker.main.userAccounts.count > 1 {\n                    communityPicker\n                        .frame(maxWidth: .infinity, alignment: .leading)\n                    accountPicker\n                        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n                } else {\n                    communityPicker\n                }\n            }\n            if isMoreThanOneTarget {\n                Group {\n                    switch target.sendState {\n                    case .unsent:\n                        EmptyView()\n                    case .sent:\n                        Image(icon: .general.success)\n                            .foregroundStyle(.themedPositive)\n                    case .failed:\n                        Image(icon: .general.error)\n                            .foregroundStyle(.themedNegative)\n                    }\n                }\n                .symbolVariant(.circle.fill)\n                .symbolRenderingMode(.hierarchical)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var communityPicker: some View {\n        Button {\n            navigation.openSheet(.communityPicker(\n                api: target.account.api,\n                callback: { target.community = .init($0) }\n            ))\n        } label: {\n            let singleAccount = AccountsTracker.main.userAccounts.count == 1\n            HStack(spacing: 0) {\n                if let community = target.community {\n                    FullyQualifiedLabelView(community, labelStyle: singleAccount ? .medium : .large)\n                } else {\n                    HStack(spacing: 7) {\n                        Image(icon: .lemmy.community)\n                            .resizable()\n                            .symbolVariant(.circle.fill)\n                            .symbolRenderingMode(.hierarchical)\n                            .aspectRatio(1, contentMode: .fit)\n                            .frame(\n                                width: singleAccount ? Constants.main.mediumAvatarSize : Constants.main.largeAvatarSize\n                            )\n                        Text(\"Choose a community...\")\n                            .font(.footnote)\n                            .multilineTextAlignment(.leading)\n                            .environment(\\._lineHeightMultiple, 0.8)\n                            .offset(y: 1)\n                    }\n                }\n                if !singleAccount || isMoreThanOneTarget {\n                    Spacer()\n                }\n            }\n            .padding(.vertical, 6)\n            .padding(.horizontal, 8)\n            .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        }\n    }\n    \n    @ViewBuilder\n    var accountPicker: some View {\n        HStack {\n            AccountPickerMenu(account: $target.account) {\n                FullyQualifiedLabelView(target.account, labelStyle: .large)\n                    .frame(maxWidth: .infinity, alignment: .leading)\n                    .padding(.vertical, 6)\n                    .padding(.horizontal, 8)\n            }\n            .onChange(of: target.account) {\n                Task {\n                    await resolveCommunity()\n                }\n            }\n            switch target.resolutionState {\n            case .notFound, .error:\n                Image(icon: .general.warning)\n                    .imageScale(.large)\n                    .symbolVariant(.fill)\n                    .symbolRenderingMode(.hierarchical)\n                    .foregroundStyle(.themedCaution)\n                    .fontWeight(.semibold)\n            default:\n                EmptyView()\n            }\n        }\n    }\n    \n    @MainActor\n    func resolveCommunity() async {\n        guard target.community?.api !== target.account.api else { return }\n        guard let community = target.community else { return }\n        \n        target.resolutionState = .resolving\n        do {\n            let newCommunity: Community = try await target.account.api.getCommunity(url: community.allResolvableUrls[0])\n            target.community = newCommunity\n            target.resolutionState = .success\n        } catch ApiClientError.noEntityFound {\n            target.resolutionState = .notFound\n        } catch {\n            target.resolutionState = .error(.init(error: error))\n        }\n    }\n}\n\n@Observable\nclass PostEditorTarget: Identifiable {\n    enum ResolutionState: Equatable {\n        case success, notFound, error(ErrorDetails), resolving\n    }\n    \n    enum SendState: Equatable {\n        case unsent, sent, failed\n    }\n    \n    var community: Community?\n    var account: UserAccount {\n        didSet {\n            slurRegex_ = nil\n            onAccountChange()\n        }\n    }\n\n    let id = UUID()\n    \n    var resolutionState: ResolutionState = .success\n    var sendState: SendState = .unsent\n    \n    var onAccountChange: () -> Void\n    \n    private var slurRegex_: Regex<AnyRegexOutput>?\n    var slurRegex: Regex<AnyRegexOutput>? {\n        get async throws {\n            if let slurRegex_ { return slurRegex_ }\n            slurRegex_ = try await account.api.getMyInstance().slurRegex()\n            return slurRegex_\n        }\n    }\n    \n    init(\n        community: Community? = nil,\n        account: UserAccount,\n        onAccountChange: @escaping () -> Void = {}\n    ) {\n        self.community = community\n        self.account = account\n        self.slurRegex_ = account.api.myInstance?.slurRegex()\n        self.onAccountChange = onAccountChange\n    }\n    \n    /// If this target matches the given feedLoader, prepends the given post\n    func prepend(post: Post, to feedLoader: (any FeedLoading)?) {\n        guard let feedLoader else { return }\n        \n        if let community,\n           let communityFeedLoader = feedLoader as? CommunityPostFeedLoader,\n           communityFeedLoader.community.actorId == community.actorId {\n            Task { @MainActor in\n                withAnimation {\n                    communityFeedLoader.prependItem(post)\n                }\n            }\n            return\n        }\n        \n        if let personContentFeedLoader = feedLoader as? SingleSourceMixedFeedLoader,\n           personContentFeedLoader.userId == account.id,\n           personContentFeedLoader.api == account.api {\n            Task { @MainActor in\n                withAnimation {\n                    personContentFeedLoader.prependItem(.init(wrappedValue: .post(post)))\n                }\n            }\n            return\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/PostEditor/PostEditorView+ImageView.swift",
    "content": "//\n//  PostEditorView+ImageView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 29/08/2024.\n//\n\nimport MlemMiddleware\nimport PhotosUI\nimport SwiftUI\n\nextension PostEditorView {\n    var imageView: some View {\n        PostEditorImageUploadWidgetView(primaryApi: primaryApi, imageUrl: $imageUrl, imageManager: $imageManager)\n    }\n}\n\nstruct PostEditorImageUploadWidgetView: View {\n    @Environment(NavigationLayer.self) var navigation\n    \n    @ScaledMetric(relativeTo: .subheadline) var buttonHeight: CGFloat = 40\n    \n    let primaryApi: ApiClient\n    @Binding var imageUrl: URL?\n    @Binding var imageManager: ImageUploadManager?\n    \n    @ViewBuilder\n    var body: some View {\n        VStack(spacing: 0) {\n            switch imageManager?.state {\n            case let .done(image):\n                uploadedImageView(url: image.url) {\n                    Task {\n                        do {\n                            try await image.delete()\n                        } catch {\n                            handleError(error, silent: true)\n                        }\n                    }\n                }\n                .transition(.opacity)\n            case let .uploading(progress: progress):\n                uploadingProgressView(progress: progress)\n                    .transition(.opacity)\n            default:\n                if let imageUrl, imageManager?.state != .idle {\n                    uploadedImageView(url: imageUrl)\n                } else {\n                    imageWaitingView\n                }\n            }\n        }\n        .background(.themedAccent.opacity(imageUrl != nil || imageManager?.image != nil ? 0 : 0.2))\n        // This second background is to prevent the view from being partially see-through, which makes the animations cleaner\n        .background(.themedGroupedBackground)\n        .clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n        .onTapGesture {\n            imageManager = imageManager ?? .init()\n        }\n    }\n    \n    @ViewBuilder\n    private func uploadedImageView(url: URL, onRemove: @escaping () -> Void = {}) -> some View {\n        MediaView(\n            url: url,\n            aspectRatioBounds: .imageDefault,\n            cornerRadius: Constants.main.mediumItemCornerRadius\n        )\n        .overlay(alignment: .topTrailing) {\n            Button(\"Remove\", systemImage: Icons.closeCircleFill) {\n                onRemove()\n                imageManager = nil\n                imageUrl = nil\n            }\n            .symbolRenderingMode(.palette)\n            .foregroundStyle(.secondary, .thinMaterial)\n            .font(.title)\n            .labelStyle(.iconOnly)\n            .padding()\n        }\n    }\n    \n    @ViewBuilder\n    private var imageWaitingView: some View {\n        VStack(spacing: 8) {\n            HStack {\n                HStack {\n                    if imageManager?.state == nil {\n                        Image(icon: .markdown.uploadImage)\n                    }\n                    Text(imageManager?.state == nil ? \"Add Image\" : \"Add an image...\")\n                }\n                .geometryGroup()\n                .padding(.leading, 4)\n                .frame(maxWidth: .infinity, alignment: imageManager?.state == nil ? .center : .leading)\n                if imageManager?.state != nil {\n                    Button(\"Remove\", systemImage: Icons.closeCircleFill) {\n                        imageManager = nil\n                    }\n                    .font(.title2)\n                    .labelStyle(.iconOnly)\n                    .symbolRenderingMode(.hierarchical)\n                    .foregroundStyle(.themedAccent)\n                }\n            }\n            .foregroundStyle(.themedAccent)\n            if imageManager?.state != nil {\n                VStack {\n                    uploadOptionsView(height: buttonHeight + 14)\n                        .transition(.asymmetric(\n                            insertion: .scale.combined(with: .opacity),\n                            removal: .move(edge: .bottom).combined(with: .opacity)\n                        ))\n                }\n            }\n        }\n        .fontWeight(.semibold)\n        .frame(maxWidth: .infinity)\n        .padding(8)\n    }\n    \n    @ViewBuilder\n    func uploadingProgressView(progress: Double) -> some View {\n        VStack {\n            Text(\"Uploading...\")\n                .foregroundStyle(.themedAccent)\n            if progress == 1.0 {\n                ProgressView()\n            } else {\n                ProgressView(value: progress)\n                    .progressViewStyle(.linear)\n                    .frame(maxWidth: .infinity)\n                    .padding([.bottom, .horizontal], 4)\n            }\n        }\n        .frame(maxWidth: .infinity)\n        .padding(8)\n    }\n    \n    @ViewBuilder\n    func uploadOptionsView(height: CGFloat) -> some View {\n        HStack {\n            Button(\"Photos\", icon: .general.photoLibary) {\n                guard let imageManager else { return }\n                navigation.showPhotosPicker(for: imageManager, api: primaryApi)\n            }\n            Button(\"Files\", icon: .general.chooseFile) {\n                guard let imageManager else { return }\n                navigation.showFilePicker(for: imageManager, api: primaryApi)\n            }\n            Button(\"Paste\", icon: .general.paste) {\n                guard let imageManager else { return }\n                navigation.uploadImageFromClipboard(for: imageManager, api: primaryApi)\n            }\n        }\n        .font(.subheadline)\n        .buttonStyle(ImageSourceButtonStyle(height: height))\n    }\n}\n\nprivate struct ImageSourceButtonStyle: ButtonStyle {\n    let height: CGFloat\n    \n    func makeBody(configuration: Self.Configuration) -> some View {\n        configuration.label\n            .labelStyle(ImageSourceButtonLabelStyle())\n            .foregroundStyle(.themedContrastingLabel)\n            .frame(maxWidth: .infinity)\n            .frame(height: height)\n            .background(.themedAccent, in: .rect(cornerRadius: 8))\n    }\n}\n\nprivate struct ImageSourceButtonLabelStyle: LabelStyle {\n    func makeBody(configuration: Configuration) -> some View {\n        VStack(spacing: 4) {\n            configuration.icon\n            configuration.title\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/PostEditor/PostEditorView+LinkView.swift",
    "content": "//\n//  PostEditorView+LinkView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 29/08/2024.\n//\n\nimport MlemMiddleware\nimport OpenGraph\nimport SwiftUI\n\nextension PostEditorView {\n    @ViewBuilder\n    func addLinkButton() -> some View {\n        Label(\"Add Link\", icon: .general.link)\n            .lineLimit(1)\n            .fontWeight(.semibold)\n            .foregroundStyle(.themedAccent)\n            .padding(.leading, 8)\n            .frame(\n                maxWidth: .infinity,\n                alignment: link == .none ? .center : .leading\n            )\n            .padding(8)\n            .background(.themedAccent.opacity(0.2))\n            // This second background is to prevent the view from being partially see-through, which makes the animations cleaner\n            .background(.themedGroupedBackground)\n            .clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n            .onTapGesture { pasteLink() }\n    }\n    \n    private func pasteLink() {\n        let url: URL?\n        if let pastedUrl = UIPasteboard.general.url {\n            url = pastedUrl\n        } else if let pastedString = UIPasteboard.general.string, pastedString.starts(with: \"http\") {\n            url = URL(string: pastedString, encodingInvalidCharacters: false)\n        } else {\n            ToastModel.main.add(.urlCopyError)\n            return\n        }\n        if let url {\n            Task {\n                do {\n                    if let api = targets.first?.account.api {\n                        link = try await .value(api.getPostLinkOrUseOpenGraph(url: url))\n                    }\n                } catch {\n                    link = .value(.init(content: url, thumbnail: nil, label: url.absoluteString))\n                    handleError(error, silent: true)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/PostEditor/PostEditorView+Logic.swift",
    "content": "//\n//  PostEditorView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 20/08/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension PostEditorView {\n    var minTextEditorHeight: CGFloat {\n        UIFont.preferredFont(forTextStyle: .body).lineHeight * 4 + 15\n    }\n    \n    var minTitleEditorHeight: CGFloat {\n        UIFont.preferredFont(forTextStyle: .title2).lineHeight + 15\n    }\n    \n    var attachmentTransition: AnyTransition {\n        .asymmetric(insertion: .scale.combined(with: .opacity), removal: .opacity)\n    }\n    \n    var canDismiss: Bool {\n        titleIsEmpty\n            && contentIsEmpty\n            && targets.count == 1\n            && link == .none\n            && imageManager == nil\n    }\n    \n    var canSubmit: Bool {\n        if !(imageManager?.state.isDone ?? true) ||\n            link == .waiting ||\n            !titleSlurMatches.isEmpty ||\n            !bodySlurMatches.isEmpty { return false }\n        if postToEdit != nil { return true }\n        return !titleIsEmpty && targets.allSatisfy { $0.community != nil && $0.resolutionState == .success }\n    }\n    \n    // ApiClient for uploading images etc\n    var primaryApi: ApiClient {\n        targets.first?.account.api ?? appState.firstApi\n    }\n    \n    @MainActor\n    func submit() async {\n        uploadHistory.deleteWhereNotPresent(in: contentTextView.text)\n        if postToEdit != nil {\n            editPost()\n        } else {\n            await send()\n        }\n    }\n    \n    private func editPost() {\n        guard let post = postToEdit else { return }\n        do {\n            try post.edit(\n                title: titleTextView.text,\n                content: contentTextView.text,\n                linkUrl: imageManager?.image?.url ?? link.url ?? imageUrl,\n                altText: post.altText,\n                thumbnail: thumbnailManager.image?.url,\n                nsfw: hasNsfwTag,\n                languageId: nil\n            )\n            hapticManager.play(haptic: .success, tier: .low)\n            dismiss()\n        } catch {\n            handleError(error)\n            sending = false\n        }\n    }\n    \n    private func send() async {\n        let validTargets = targets.filter { $0.sendState != .sent }\n        let posts = await withTaskGroup(\n            of: (target: PostEditorTarget, post: Post?).self,\n            returning: [Post].self\n        ) { taskGroup in\n            for target in validTargets {\n                if let community = target.community as? Community {\n                    taskGroup.addTask { @MainActor in\n                        let post: Post?\n                        do {\n                            guard community.api === target.account.api else {\n                                assertionFailure()\n                                throw PostEditorViewError.mismatchingTargetApi\n                            }\n                            post = try await community.api.createPost(\n                                communityId: community.id,\n                                title: titleTextView.text,\n                                content: contentTextView.text,\n                                linkUrl: imageManager?.image?.url ?? link.url ?? imageUrl,\n                                thumbnail: thumbnailManager.image?.url,\n                                nsfw: hasNsfwTag\n                            )\n                        } catch {\n                            handleError(error, silent: true)\n                            post = nil\n                        }\n                        return (target, post)\n                    }\n                }\n            }\n            \n            var posts = [Post]()\n            \n            while let result = await taskGroup.next() {\n                if let post = result.post {\n                    posts.append(post)\n                    result.target.prepend(post: post, to: feedLoader)\n                    if self.targets.count == 1 {\n                        result.target.sendState = .sent\n                    }\n                } else {\n                    result.target.sendState = .failed\n                }\n            }\n            return posts\n        }\n        if posts.count == validTargets.count {\n            hapticManager.play(haptic: .success, tier: .low)\n            dismiss()\n        } else {\n            sending = false\n        }\n    }\n    \n    var animationHashValue: Int {\n        var hasher = Hasher()\n        hasher.combine(link)\n        hasher.combine(imageManager)\n        hasher.combine(hasNsfwTag)\n        return hasher.finalize()\n    }\n    \n    func restoreFocusState() {\n        switch lastFocusedField {\n        case .title:\n            titleTextView.becomeFirstResponder()\n        case .content:\n            contentTextView.becomeFirstResponder()\n        case nil:\n            break\n        }\n    }\n    \n    func saveFocusState() {\n        if contentTextView.isFirstResponder {\n            lastFocusedField = .content\n        } else if titleTextView.isFirstResponder {\n            lastFocusedField = .title\n        } else {\n            lastFocusedField = nil\n        }\n    }\n    \n    func checkSlurFilter(text: String, slurMatches: Binding<[String: String]>) {\n        Task {\n            let matches = await findSlurFilterMatches(text: text)\n            Task { @MainActor in\n                slurMatches.wrappedValue = matches\n            }\n        }\n    }\n    \n    func checkSlurFilters() {\n        checkSlurFilter(text: contentTextView.text, slurMatches: $bodySlurMatches)\n        checkSlurFilter(text: titleTextView.text, slurMatches: $titleSlurMatches)\n    }\n    \n    /// Checks if the given text fails `slurRegex` and updates the given `String?` binding to the current\n    /// validation state\n    func findSlurFilterMatches(text: String) async -> [String: String] {\n        var newSlurMatches: [String: String] = .init()\n        \n        for target in targets {\n            let host = target.account.host\n            guard newSlurMatches[host] == nil else { continue }\n            \n            do {\n                if let output = try await target.slurRegex?.firstMatch(in: text.lowercased()) {\n                    newSlurMatches[host] = String(text[output.range])\n                }\n            } catch {\n                handleError(error, silent: true)\n            }\n        }\n        \n        return newSlurMatches\n    }\n}\n\nprivate enum PostEditorViewError: Error {\n    case mismatchingTargetApi\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/PostEditor/PostEditorView+Toolbar.swift",
    "content": "//\n//  PostEditorView+Toolbar.swift\n//  Mlem\n//\n//  Created by Sjmarf on 02/09/2024.\n//\n\nimport ComponentViews\nimport SwiftUI\n\nextension PostEditorView {\n    @ToolbarContentBuilder\n    var toolbar: some ToolbarContent {\n        ToolbarItem(placement: .topBarLeading) {\n            CloseButtonView(ios18Label: .cancel, requiresConfirmation: !canDismiss) {\n                dismiss()\n            }\n        }\n        ToolbarItemGroup(placement: .topBarTrailing) {\n            Menu(\"Add\", icon: .general.add) {\n                Toggle(\"NSFW Tag\", icon: .lemmy.tag, isOn: $hasNsfwTag)\n                if postToEdit == nil {\n                    Button(\"Crosspost\", systemImage: \"shuffle\") {\n                        if let account = targets.last?.account {\n                            let newTarget: PostEditorTarget = .init(account: account, onAccountChange: checkSlurFilters)\n                            targets.append(newTarget)\n                            navigation.openSheet(.communityPicker(api: account.api, callback: { community in\n                                newTarget.community = community\n                            }))\n                        }\n                    }\n                }\n            }\n            if self.sending {\n                ProgressView()\n            } else {\n                sendButton\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var sendButton: some View {\n        Button(\"Send\", icon: postToEdit != nil ? .general.success : .lemmy.send) {\n            self.sending = true\n            Task { await submit() }\n        }\n        .disabled(!canSubmit)\n        .glassProminentButtonStyle()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/PostEditor/PostEditorView+Views.swift",
    "content": "//\n//  PostEditorView+Views.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/09/2024.\n//\n\nimport SwiftUI\n\nextension PostEditorView {\n    @ViewBuilder\n    var attachmentPickerView: some View {\n        switch link {\n        case let .value(link):\n            PostEditorWebsitePreviewView(\n                link: .init(\n                    get: { link },\n                    set: { self.link = .value($0) }\n                ),\n                imageManager: $thumbnailManager,\n                primaryApi: primaryApi,\n                removeCallback: {\n                    self.link = .none\n                },\n                shouldBlur: false\n            )\n            .transition(.scale.combined(with: .opacity))\n        default:\n            HStack(spacing: 10) {\n                if imageManager == nil, imageUrl == nil {\n                    addLinkButton()\n                        .transition(.move(edge: .leading).combined(with: .opacity))\n                }\n                if link == .none {\n                    imageView\n                        .transition(.move(edge: .trailing).combined(with: .opacity))\n                }\n            }\n            .transition(.scale.combined(with: .opacity))\n        }\n    }\n    \n    @ViewBuilder\n    var targetSelectionView: some View {\n        let showWarning = !targets.allSatisfy { $0.sendState != .failed }\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            if let postToEdit {\n                ExpectedView(postToEdit.community) { community in\n                    FullyQualifiedLinkView(community, labelStyle: .medium)\n                        .padding(.horizontal, Constants.main.standardSpacing)\n                } placeholder: {\n                    Text(verbatim: .communityPlaceholder).redacted(reason: .placeholder)\n                }\n            } else {\n                ForEach(Array(targets.enumerated()), id: \\.element.id) { index, target in\n                    HStack(spacing: Constants.main.standardSpacing) {\n                        PostEditorTargetView(target: target, isMoreThanOneTarget: targets.count > 1)\n                            .frame(maxWidth: .infinity, alignment: .leading)\n                        if targets.count > 1 {\n                            Button(\"Remove\", icon: .general.close) {\n                                targets.remove(at: index)\n                                checkSlurFilters()\n                            }\n                            .symbolVariant(.circle.fill)\n                            .symbolRenderingMode(.hierarchical)\n                            .imageScale(.large)\n                            .labelStyle(.iconOnly)\n                        }\n                    }\n                    .frame(maxWidth: .infinity, alignment: .leading)\n                }\n            }\n            if showWarning {\n                Text(targets.count == 1 ? \"Post failed to send.\" : \"One of more of your posts failed to send.\")\n                    .multilineTextAlignment(.center)\n                    .padding(.vertical, 3)\n                    .frame(maxWidth: .infinity)\n                    .background(.opacity(0.2), in: .capsule)\n                    .foregroundStyle(.themedNegative)\n                    .padding(.horizontal)\n            }\n        }\n        .animation(.easeOut(duration: 0.2), value: showWarning)\n    }\n \n    @ViewBuilder\n    var nsfwTagView: some View {\n        Button {\n            hasNsfwTag = false\n        } label: {\n            HStack {\n                Text(\"NSFW\")\n                    .font(.footnote)\n                    .fontWeight(.black)\n                    .foregroundStyle(.themedContrastingLabel)\n                Image(icon: .general.close)\n                    .foregroundStyle(.opacity(0.8))\n            }\n            .foregroundStyle(.white)\n            .padding(.vertical, 2)\n            .padding(.horizontal, 8)\n            .background(.themedWarning, in: .capsule)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/PostEditor/PostEditorView.swift",
    "content": "//\n//  PostEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 12/08/2024.\n//\n\nimport ComponentViews\nimport Haptics\nimport MlemMiddleware\nimport PhotosUI\nimport SwiftUI\n\nstruct PostEditorView: View {\n    enum Field { case title, content }\n    enum LinkState: Hashable {\n        case none, waiting, value(PostLink)\n        \n        var url: URL? {\n            switch self {\n            case let .value(link): link.content\n            default: nil\n            }\n        }\n    }\n    \n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.dismiss) var dismiss\n    \n    @State var titleTextView: UITextView\n    @State var contentTextView: UITextView\n    \n    @State var postToEdit: Post?\n    @State var presentationSelection: PresentationDetent = .large\n    @State var titleIsEmpty: Bool = true\n    @State var contentIsEmpty: Bool = true\n    @State var lastFocusedField: Field? = .title\n    @State var hasNsfwTag: Bool = false\n    @State var link: LinkState = .none\n    @State var imageUrl: URL?\n    @State var imageManager: ImageUploadManager?\n    @State var thumbnailManager: ImageUploadManager = .init()\n    @State var uploadHistory: ImageUploadHistoryManager = .init()\n    @State var markdownToolbarEditorModel: MarkdownEditorToolbarModel = .init()\n    @State var sending: Bool = false\n        \n    @State var targets: [PostEditorTarget]\n    \n    @State var titleSlurMatches: [String: String] = .init()\n    @State var bodySlurMatches: [String: String] = .init()\n    \n    var feedLoader: (any FeedLoading)?\n    \n    /// Initializer for editing a post\n    init?(\n        postToEdit: Post,\n        community: Community?\n    ) {\n        self.init(\n            community: community,\n            title: postToEdit.title,\n            content: postToEdit.content,\n            type: postToEdit.type,\n            nsfw: postToEdit.nsfw,\n            feedLoader: nil\n        )\n        self.postToEdit = postToEdit\n    }\n    \n    /// Initializer for creating a post\n    init?(\n        community: Community?,\n        title: String = \"\",\n        content: String? = nil,\n        type: PostType? = nil,\n        nsfw: Bool = false,\n        feedLoader: (any FeedLoading)?\n    ) {\n        if let account = (AppState.main.firstAccount as? UserAccount) {\n            self._targets = .init(wrappedValue: [.init(community: community, account: account)])\n        } else {\n            return nil\n        }\n        self.feedLoader = feedLoader\n        self.titleTextView = .init()\n        self.contentTextView = .init()\n        titleTextView.tag = 0\n        contentTextView.tag = 1\n        \n        titleTextView.text = title\n        contentTextView.text = content ?? \"\"\n        self._titleIsEmpty = .init(wrappedValue: title.isEmpty)\n        self._hasNsfwTag = .init(wrappedValue: nsfw)\n        \n        switch type {\n        case let .media(url):\n            self._imageUrl = .init(wrappedValue: url)\n        case let .embedded(_, url):\n            self._link = .init(wrappedValue: .value(.init(content: url, thumbnail: nil, label: \"\")))\n        case let .link(url):\n            self._link = .init(wrappedValue: .value(url))\n        case .titleOnly, .text, .poll, nil:\n            break\n        }\n    }\n    \n    var body: some View {\n        CollapsibleSheetView(presentationSelection: $presentationSelection, canDismiss: canDismiss) {\n            NavigationStack {\n                contentView\n                    .navigationBarTitleDisplayMode(.inline)\n                    .toolbar { toolbar }\n                    .background(.themedGroupedBackground)\n            }\n            .presentationBackground(.themedGroupedBackground)\n            .onAppear {\n                contentTextView.resignFirstResponder()\n                titleTextView.becomeFirstResponder()\n            }\n        }\n        .onAppear {\n            targets.first?.onAccountChange = checkSlurFilters\n        }\n        .onChange(of: imageManager?.image) {\n            imageUrl = imageManager?.image?.url\n        }\n        .onChange(of: presentationSelection) {\n            if presentationSelection == .large {\n                restoreFocusState()\n            } else {\n                saveFocusState()\n            }\n        }\n        .onChange(of: navigation.isTopSheet) {\n            if navigation.isTopSheet {\n                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: restoreFocusState)\n            } else {\n                saveFocusState()\n            }\n        }\n        .onChange(of: sending) {\n            if sending {\n                titleTextView.resignFirstResponder()\n                titleTextView.isEditable = false\n                contentTextView.resignFirstResponder()\n                contentTextView.isEditable = false\n            } else {\n                titleTextView.isEditable = true\n                contentTextView.isEditable = true\n            }\n        }\n        .onDisappear {\n            if !navigation.isAlive, !sending {\n                Task {\n                    do {\n                        try await imageManager?.image?.delete()\n                        try await thumbnailManager.image?.delete()\n                    } catch {\n                        handleError(error, silent: true)\n                    }\n                }\n                uploadHistory.deleteAll()\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var contentView: some View {\n        ScrollView {\n            VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n                targetSelectionView\n                \n                if postToEdit == nil {\n                    Line()\n                        .stroke(style: StrokeStyle(lineWidth: 2, dash: [5]))\n                        .frame(height: 2)\n                        .foregroundStyle(.themedPrimary.opacity(0.2))\n                        // The line isn't centered properly due to the way that SwiftUI shapes work; this fixes it\n                        .padding(.bottom, -1)\n                        .padding(.top, 1)\n                }\n                \n                VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n                    VStack(spacing: Constants.main.standardSpacing) {\n                        MarkdownTextEditor(\n                            onChange: {\n                                // Avoid unnecessary view update\n                                if titleIsEmpty != $0.isEmpty {\n                                    titleIsEmpty = $0.isEmpty\n                                }\n                                checkSlurFilter(text: $0, slurMatches: $titleSlurMatches)\n                            },\n                            prompt: \"Title\",\n                            textView: titleTextView,\n                            font: .preferredFont(forTextStyle: .title2),\n                            content: {\n                                MarkdownEditorToolbarView(\n                                    showing: .inlineOnly,\n                                    textView: titleTextView,\n                                    model: .init()\n                                )\n                            }\n                        )\n                        .frame(\n                            maxWidth: .infinity,\n                            minHeight: minTitleEditorHeight,\n                            maxHeight: .infinity,\n                            alignment: .topLeading\n                        )\n  \n                        if !titleSlurMatches.isEmpty {\n                            FilterViolationWarning(failures: titleSlurMatches)\n                                .padding(.horizontal, Constants.main.standardSpacing)\n                                .padding(.bottom, Constants.main.standardSpacing)\n                        }\n                    }\n                    .padding(.top, Constants.main.halfSpacing)\n                    .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n                    \n                    if hasNsfwTag {\n                        nsfwTagView\n                            .padding(.leading, 10)\n                            .transition(attachmentTransition)\n                    }\n                    \n                    attachmentPickerView\n                    \n                    VStack {\n                        MarkdownTextEditor(\n                            onChange: {\n                                // Avoid unnecessary view update\n                                if contentIsEmpty != $0.isEmpty {\n                                    contentIsEmpty = $0.isEmpty\n                                }\n                                checkSlurFilter(text: $0, slurMatches: $bodySlurMatches)\n                            },\n                            prompt: \"Optional Description\",\n                            textView: contentTextView,\n                            content: {\n                                MarkdownEditorToolbarView(\n                                    textView: contentTextView,\n                                    uploadHistory: uploadHistory,\n                                    model: markdownToolbarEditorModel\n                                )\n                            }\n                        )\n                        .onChange(of: primaryApi, initial: true) {\n                            markdownToolbarEditorModel.imageUploadApi = primaryApi\n                        }\n                        .frame(\n                            maxWidth: .infinity,\n                            minHeight: minTextEditorHeight,\n                            maxHeight: .infinity,\n                            alignment: .topLeading\n                        )\n  \n                        if !bodySlurMatches.isEmpty {\n                            FilterViolationWarning(failures: bodySlurMatches)\n                                .padding(.horizontal, Constants.main.standardSpacing)\n                                .padding(.bottom, Constants.main.standardSpacing)\n                        }\n                    }\n                    .padding([.vertical, .bottom], Constants.main.standardSpacing)\n                    .background(\n                        .themedSecondaryGroupedBackground,\n                        in: UnevenRoundedRectangle(cornerRadii: .init(\n                            topLeading: Constants.main.standardSpacing,\n                            bottomLeading: Constants.main.standardSpacing,\n                            bottomTrailing: Constants.main.standardSpacing,\n                            topTrailing: Constants.main.standardSpacing\n                        ))\n                    )\n                }\n            }\n            .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n            .animation(.snappy(duration: 0.2, extraBounce: 0.05), value: animationHashValue)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommentEditor/PostEditor/PostEditorWebsitePreviewView.swift",
    "content": "//\n//  PostEditorWebsitePreviewView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-07.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct PostEditorWebsitePreviewView: View {\n    @Environment(\\.palette) var palette\n\n    @Setting(\\.post_webPreview_showIcon) var showFavicons\n    @Setting(\\.behavior_muteVideos) var muteVideos\n    \n    @Binding var link: PostLink\n    @Binding var imageManager: ImageUploadManager\n\n    @State var isEditing: Bool = false\n\n    let primaryApi: ApiClient\n    let removeCallback: () -> Void\n    let shouldBlur: Bool\n    \n    var body: some View {\n        content\n            .frame(maxWidth: .infinity, alignment: .leading)\n            .background(.themedSecondaryGroupedBackground)\n            .clipShape(RoundedRectangle(cornerRadius: Constants.main.mediumItemCornerRadius))\n            .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.mediumItemCornerRadius))\n            .paletteBorder(cornerRadius: Constants.main.mediumItemCornerRadius)\n            .contentShape(.rect)\n    }\n\n    @ViewBuilder\n    var content: some View {\n        VStack(alignment: .leading, spacing: 0) {\n            if isEditing {\n                LinkEditorView(url: link.content, api: primaryApi) { link in\n                    self.link = link\n                    withAnimation(.easeOut(duration: 0.2)) {\n                        isEditing = false\n                    }\n                }\n            } else if let thumbnailUrl = imageManager.image?.url ?? link.effectiveThumbnail {\n                imageView(thumbnailUrl)\n                footerView(withLinkHost: false, withRemoveButton: false)\n            } else if primaryApi.supports(.customPostThumbnail, defaultValue: false) {\n                imagePlaceholderView\n                footerView(withLinkHost: true, withRemoveButton: false)\n            } else {\n                footerView(withLinkHost: true, withRemoveButton: true)\n            }\n        }\n    }\n\n    @ViewBuilder\n    func footerView(withLinkHost showLinkHost: Bool, withRemoveButton showRemoveButton: Bool) -> some View {\n        HStack {\n            VStack(alignment: .leading, spacing: 5) {\n                if showLinkHost {\n                    LinkHostView(link: link, withCapsule: false)\n                        .padding([.horizontal, .top], Constants.main.standardSpacing)\n                }\n                titleView\n            }\n            Spacer()\n            HStack {\n                editButton\n                .padding(.vertical, 10)\n                if showRemoveButton {\n                    removeButton\n                }\n            }\n            .foregroundStyle(.secondary, .themedTertiaryGroupedBackground)\n            .padding(.trailing, 10)\n        }\n    }\n\n    @ViewBuilder\n    var titleView: some View {\n        Text(link.label)\n            .font(.subheadline)\n            .fontWeight(.semibold)\n            .padding(Constants.main.standardSpacing)\n            .foregroundStyle(.themedPrimary)\n    }\n    \n    @ViewBuilder\n    func imageView(_ thumbnailUrl: URL) -> some View {\n        MediaView(\n            url: thumbnailUrl,\n            controlState: .constant(.init(\n                blurred: shouldBlur,\n                animating: false,\n                muted: muteVideos\n            )),\n            aspectRatioBounds: .bounded(vertical: .init(width: 1, height: 1), horizontal: nil),\n            contentMode: .fill,\n            overlays: shouldBlur ? [.controls, .nsfw, .error] : [.controls, .error]\n        )\n        .overlay(alignment: .bottomLeading) {\n            LinkHostView(link: link, withCapsule: true)\n                .padding(Constants.main.halfSpacing)\n        }\n        .overlay(alignment: .topLeading) {\n            if primaryApi.supports(.customPostThumbnail, defaultValue: false) {\n                thumbnailUploadButton\n                    .padding(10)\n            }\n        }\n        .overlay(alignment: .topTrailing) {\n            removeButton\n                .padding(10)\n                .foregroundStyle(.secondary, .thinMaterial)\n        }\n    }\n\n    @ViewBuilder\n    var imagePlaceholderView: some View {\n        ZStack {\n            ThemedColor.themedAccent.resolve(with: palette).opacity(0.2)\n                .frame(maxWidth: .infinity)\n                .aspectRatio(5 / 3, contentMode: .fit)\n            VStack {\n                Image(icon: .general.photoLibary)\n                    .resizable()\n                    .aspectRatio(contentMode: .fit)\n                    .foregroundStyle(.themedAccent.opacity(0.2))\n                    .frame(width: 80)\n                Text(\"No image\")\n                    .foregroundStyle(.themedAccent.opacity(0.5))\n                    .fontWeight(.semibold)\n                ImageUploadMenu(imageManager: imageManager, imageUploadApi: primaryApi) {\n                    Text(\"Upload\")\n                }\n                .font(.footnote)\n                .buttonStyle(.borderedProminent)\n                .padding(.top, 10)\n            }\n        }\n        .overlay(alignment: .topTrailing) {\n            removeButton\n                .padding(10)\n                .foregroundStyle(.themedAccent, .themedAccent.opacity(0.2))\n        }\n    }\n\n    @ViewBuilder\n    var removeButton: some View {\n        Button(\n            \"Remove\",\n            icon: .general.close\n        ) {\n            Task {\n                do {\n                    try await imageManager.image?.delete()\n                    removeCallback()\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n        .buttonStyle(OverlayButtonStyle())\n    }\n\n    @ViewBuilder\n    var editButton: some View {\n        Button(\"Edit link\", icon: .general.link) {\n            withAnimation(.easeOut(duration: 0.2)) {\n                isEditing = true\n            }\n        }\n        .buttonStyle(OverlayButtonStyle())\n    }\n\n    @ViewBuilder\n    var thumbnailUploadButton: some View {\n        if imageManager.image != nil {\n            Button(\"Custom Thumbnail\", icon: .general.close) {\n                Task {\n                    do {\n                        try await imageManager.delete()\n                    } catch {\n                        handleError(error)\n                    }\n                }\n            }\n            .fontWeight(.semibold)\n            .padding(.vertical, 2)\n            .padding(.horizontal, 8)\n            .background(.thinMaterial, in: .capsule)\n            .foregroundStyle(.secondary)\n            .padding(5)\n        } else {\n            ImageUploadMenu(imageManager: imageManager, imageUploadApi: primaryApi) {\n                Label(\"Change Thumbnail\", icon: .general.chooseImage)\n            }\n            .buttonStyle(OverlayButtonStyle())\n            .foregroundStyle(.secondary, .thinMaterial)\n        }\n    }\n}\n\nprivate struct OverlayButtonStyle: ButtonStyle {\n    func makeBody(configuration: Configuration) -> some View {\n        configuration.label\n            .font(.title)\n            .fontWeight(.semibold)\n            .imageScale(.large)\n            .labelStyle(.iconOnly)\n            .symbolVariant(.circle.fill)\n            .symbolRenderingMode(.palette)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/CommunityDescriptionEditorView.swift",
    "content": "//\n//  CommunityDescriptionEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-30.\n//  \n\nimport ComponentViews\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CommunityDescriptionEditorView: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.dismiss) var dismiss\n\n    let community: Community\n\n    @State var textView: UITextView = .init()\n    @State var textHasChanged: Bool = false\n    @State var sending: Bool = false\n    @State var markdownToolbarEditorModel: MarkdownEditorToolbarModel = .init()\n    @State var uploadHistory: ImageUploadHistoryManager = .init()\n    @State var presentationSelection: PresentationDetent = .large\n\n    init(community: Community) {\n        self.community = community\n        textView.text = community.description ?? \"\"\n    }\n\n    var body: some View {\n        CollapsibleSheetView(presentationSelection: $presentationSelection, canDismiss: !textHasChanged) {\n            NavigationStack {\n                content\n                    .navigationBarTitleDisplayMode(.inline)\n                    .toolbar {\n                        ToolbarItem(placement: .topBarLeading) {\n                            CloseButtonView(ios18Label: .cancel)\n                        }\n                        ToolbarItem(placement: .topBarTrailing) {\n                            if sending {\n                                ProgressView()\n                            } else {\n                                sendButton\n                            }\n                        }\n                    }\n            }\n        }\n        .onChange(of: presentationSelection) {\n            if presentationSelection == .large {\n                textView.becomeFirstResponder()\n            }\n        }\n        .onChange(of: navigation.isTopSheet) {\n            if navigation.isTopSheet, navigation.model != nil {\n                textView.becomeFirstResponder()\n            }\n        }\n    }\n\n    var content: some View {\n        ScrollView {\n            textEditorView\n                .frame(\n                    maxWidth: .infinity,\n                    minHeight: minTextEditorHeight,\n                    maxHeight: .infinity,\n                    alignment: .topLeading\n                )\n                .padding(.vertical, Constants.main.standardSpacing)\n                .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n                .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n                .padding(.horizontal, Constants.main.standardSpacing)\n        }\n        .background(.themedGroupedBackground)\n        .presentationBackground(.themedGroupedBackground)\n        .scrollBounceBehavior(.basedOnSize)\n    }\n\n    var textEditorView: some View {\n        MarkdownTextEditor(\n            onChange: { newValue in\n                textHasChanged = newValue != (community.description ?? \"\") \n            },\n            prompt: \"Start writing...\",\n            textView: textView,\n            content: {\n                MarkdownEditorToolbarView(\n                    textView: textView,\n                    uploadHistory: uploadHistory,\n                    model: markdownToolbarEditorModel\n                )\n            }\n        )\n    }\n\n    @ViewBuilder\n    var sendButton: some View {\n        Button(\"Send\", icon: community.description == nil ? .lemmy.send : .general.success) {\n            sending = true\n            Task(priority: .userInitiated) {\n                await send()\n            }\n        }\n        .disabled(!textHasChanged)\n        .glassProminentButtonStyle()\n    }\n\n    func send() async {\n        uploadHistory.deleteWhereNotPresent(in: textView.text)\n        community.updateDescription(textView.text) { status in\n            switch status {\n            case .success:\n                textView.resignFirstResponder()\n                textView.isEditable = false\n                hapticManager.play(haptic: .success, tier: .low)\n                dismiss()\n            case let .failure(error):\n                sending = false\n                handleError(error)\n            }\n        }\n    }\n\n    var minTextEditorHeight: CGFloat {\n        UIFont.preferredFont(forTextStyle: .body).lineHeight * 4 + 15\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/ContentPurgeEditorView.swift",
    "content": "//\n//  ContentPurgeEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-10-26.\n//\n\nimport ComponentViews\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ContentPurgeEditorView: View {\n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.dismiss) var dismiss\n    \n    let target: any PurgableProviding\n    \n    @State var community: ExpectedValue<(Community)>?\n    @State var reason: String = \"\"\n    @FocusState var reasonFocused: Bool\n    @State var presentationSelection: PresentationDetent = .large\n    \n    init(target: any PurgableProviding) {\n        self.target = target\n        self._community = .init(wrappedValue: (target as? any InteractableProviding)?.community)\n    }\n\n    var body: some View {\n        CollapsibleSheetView(presentationSelection: $presentationSelection, canDismiss: reason.isEmpty) {\n            NavigationStack {\n                Form {\n                    Section {\n                        WarningView(\n                            icon: .lemmy.purge,\n                            text: \"Purged content is erased from the database and cannot be restored.\",\n                            inList: true\n                        )\n                    }\n                    Section {\n                        TextField(\"Reason (Optional)\", text: $reason, axis: .vertical)\n                            .focused($reasonFocused)\n                    }\n                    Section {\n                        ReasonShortcutView(reason: $reason)\n                    }\n                    if let community {\n                        ExpectedView(community) { community in\n                            RulesListView(model: community, reason: $reason)\n                        }\n                    }\n                    if let instance = appState.firstSession.instance {\n                        RulesListView(model: instance, reason: $reason)\n                    }\n                }\n                .scrollDismissesKeyboard(.interactively)\n                .navigationBarTitleDisplayMode(.inline)\n                .navigationTitle(\"Purge\")\n                .toolbar {\n                    ToolbarItem(placement: .topBarLeading) {\n                        CloseButtonView(ios18Label: .cancel)\n                    }\n                    ToolbarItem(placement: .topBarTrailing) {\n                        Button(\"Send\", icon: .lemmy.send) {\n                            Task { await send() }\n                        }\n                        .glassProminentButtonStyle()\n                    }\n                }\n            }\n            .onAppear { reasonFocused = true }\n        }\n    }\n    \n    func send() async {\n        do {\n            try await target.purge(reason: reason.isEmpty ? nil : reason)\n            hapticManager.play(haptic: .success, tier: .low)\n            dismiss()\n        } catch {\n            handleError(error)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/ContentRemovalEditorView.swift",
    "content": "//\n//  ContentRemovalEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 09/10/2024.\n//\n\nimport ComponentViews\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ContentRemovalEditorView: View {\n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.dismiss) var dismiss\n    \n    enum Mode {\n        case remove, restore\n    }\n    \n    let target: any RemovableProviding\n    @State var mode: Mode\n    \n    @State var community: ExpectedValue<(Community)>?\n    @State var reason: String = \"\"\n    @FocusState var reasonFocused: Bool\n    @State var presentationSelection: PresentationDetent = .large\n    \n    init(target: any RemovableProviding) {\n        self.target = target\n        self._mode = .init(wrappedValue: target.removed ? .restore : .remove)\n        self._community = .init(wrappedValue: (target as? any InteractableProviding)?.community)\n    }\n    \n    var body: some View {\n        CollapsibleSheetView(presentationSelection: $presentationSelection, canDismiss: reason.isEmpty) {\n            NavigationStack {\n                Form {\n                    TextField(\"Reason (Optional)\", text: $reason, axis: .vertical)\n                        .focused($reasonFocused)\n                    if mode == .remove {\n                        Section {\n                            ReasonShortcutView(reason: $reason)\n                        }\n                        \n                        // ExpectedView causes rendering issues here\n                        if let community = community?.value {\n                            RulesListView(model: community, reason: $reason)\n                        }\n                        if let instance = appState.firstSession.instance {\n                            RulesListView(model: instance, reason: $reason)\n                        }\n                    }\n                }\n                .scrollDismissesKeyboard(.interactively)\n                .navigationBarTitleDisplayMode(.inline)\n                .navigationTitle(mode == .restore ? \"Restore\" : \"Remove\")\n                .toolbar {\n                    ToolbarItem(placement: .topBarLeading) {\n                        CloseButtonView(ios18Label: .cancel)\n                    }\n                    ToolbarItem(placement: .topBarTrailing) {\n                        Button(\"Send\", icon: .lemmy.send) {\n                            send()\n                        }\n                        .glassProminentButtonStyle()\n                    }\n                }\n            }\n            .onAppear { reasonFocused = true }\n        }\n    }\n    \n    func send() {\n        target.toggleRemoved(reason: reason) { status in\n            Task { @MainActor in\n                switch status {\n                case .success:\n                    hapticManager.play(haptic: .success, tier: .low)\n                    dismiss()\n                case let .failure(error):\n                    handleError(error)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/FilterViolationWarning.swift",
    "content": "//\n//  FilterViolationWarning.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-02-03.\n//\n\nimport SwiftUI\n\nstruct FilterViolationWarning: View {\n    let failures: [String: String]\n    \n    var body: some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            Label(\"Filter violation\", icon: .general.warning)\n                .font(.footnote)\n                .foregroundStyle(.themedWarning)\n                .padding(.vertical, 5)\n                .padding(.horizontal, 7)\n                .background {\n                    Capsule()\n                        .fill(.themedWarning.opacity(0.2))\n                        .stroke(.themedWarning)\n                }\n            \n            ForEach(failures.keys.sorted(), id: \\.self) { instance in\n                let failingText = Text(failures[instance] ?? \"\").fontWeight(.semibold)\n                Text(\"\\(instance) disallows \\(failingText)\")\n            }\n        }\n        .frame(maxWidth: .infinity, alignment: .leading)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/NoteEditorView.swift",
    "content": "//\n//  NoteEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-12-14.\n//\n\nimport ComponentViews\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nstruct NoteEditorView: View {\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.dismiss) var dismiss\n    \n    let person: Person\n    \n    @State var note: String\n    @FocusState var textFieldFocused: Bool\n    @State var presentationSelection: PresentationDetent = .large\n\n    init(person: Person) {\n        self.person = person\n        self.note = person.note ?? \"\"\n    }\n\n    var body: some View {\n        CollapsibleSheetView(presentationSelection: $presentationSelection, canDismiss: note.isEmpty) {\n            NavigationStack {\n                Form {\n                    TextField(\"Note\", text: $note, axis: .vertical)\n                        .focused($textFieldFocused)\n                }\n                .scrollDismissesKeyboard(.interactively)\n                .navigationBarTitleDisplayMode(.inline)\n                .navigationTitle(\"Edit Note\")\n                .toolbar {\n                    ToolbarItem(placement: .topBarLeading) {\n                        CloseButtonView(ios18Label: .cancel)\n                    }\n                    ToolbarItem(placement: .topBarTrailing) {\n                        Button(\"Save\", icon: .general.success) {\n                            Task { await send() }\n                        }\n                        .glassProminentButtonStyle()\n                    }\n                }\n            }\n            .onAppear { textFieldFocused = true }\n        }\n    }\n    \n    func send() async {\n        person.updateNote(content: note.isEmpty ? nil : note)\n        hapticManager.play(haptic: .success, tier: .low)\n        dismiss()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/PersonBanEditorView+Logic.swift",
    "content": "//\n//  PersonBanEditorView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-11-15.\n//\n\nimport Foundation\n\nextension PersonBanEditorView {\n    var dateFormatter: DateComponentsFormatter {\n        let formatter = DateComponentsFormatter()\n        formatter.unitsStyle = .abbreviated\n        formatter.maximumUnitCount = 1\n        return formatter\n    }\n    \n    func send() async {\n        do {\n            if shouldBan {\n                try await banUser()\n            } else {\n                try await unbanUser()\n            }\n            dismiss()\n        } catch {\n            handleError(error)\n        }\n    }\n    \n    private func banUser() async throws {\n        if targetInstance {\n            try await person.banFromInstance(\n                removeContent: removeContent,\n                reason: reason,\n                expires: isPermanent ? nil : expiryDate\n            )\n        } else if let community {\n            try await person.ban(\n                from: community,\n                removeContent: removeContent,\n                reason: reason,\n                expires: isPermanent ? nil : expiryDate\n            )\n        }\n    }\n    \n    private func unbanUser() async throws {\n        if targetInstance {\n            try await person.unbanFromInstance(reason: reason)\n        } else if let community {\n            try await person.unban(from: community, reason: reason)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/PersonBanEditorView.swift",
    "content": "//\n//  PersonBanEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-11-11.\n//\n\nimport ComponentViews\nimport Haptics\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct PersonBanEditorView: View {\n    enum FocusedField: Hashable {\n        case reason, days\n    }\n    \n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.dismiss) var dismiss\n    \n    let person: Person\n    let community: Community?\n    var isBannedFromCommunity: Bool\n    var shouldBan: Bool = true\n    \n    @State var targetInstance: Bool\n    @State var isPermanent: Bool = true\n    @State var expiryDate: Date = Calendar.current.date(byAdding: .day, value: 1, to: .now) ?? .now\n    @State var reason: String = \"\"\n    @State var removeContent: Bool = false\n    \n    @FocusState var focusedField: FocusedField?\n    @State var presentationSelection: PresentationDetent = .large\n    \n    var selectedTarget: (any ProfileProviding)? {\n        if targetInstance {\n            appState.firstSession.instance\n        } else {\n            community\n        }\n    }\n    \n    init(\n        person: Person,\n        community: Community?,\n        isBannedFromCommunity: Bool,\n        shouldBan: Bool\n    ) {\n        self.person = person\n        self.community = community\n        self.isBannedFromCommunity = isBannedFromCommunity\n        self.shouldBan = shouldBan\n        \n        let isCommunityModerator: Bool\n        if let community {\n            isCommunityModerator = (AppState.main.firstSession as? UserSession)?.person?.moderates?(.community(community)) ?? false\n        } else {\n            isCommunityModerator = false\n        }\n        self._targetInstance = .init(\n            wrappedValue: !(isCommunityModerator || person.bannedFromInstance == shouldBan) || isBannedFromCommunity == shouldBan\n        )\n    }\n    \n    var days: Int {\n        get {\n            Calendar.current.dateComponents(\n                [.day],\n                from: .now,\n                // This prevents the number of days ticking down if you leave the sheet open for more than a minute\n                to: expiryDate.addingTimeInterval(60 * 60)\n            ).day ?? 0\n        }\n        nonmutating set {\n            expiryDate = Calendar.current.date(byAdding: .day, value: newValue, to: .now) ?? .now\n        }\n    }\n    \n    var body: some View {\n        CollapsibleSheetView(presentationSelection: $presentationSelection, canDismiss: reason.isEmpty) {\n            NavigationStack {\n                Form {\n                    scopeSection\n                    if appState.firstApi.supports(.unbanWithReason, defaultValue: true) || shouldBan {\n                        reasonSection\n                    }\n                    if shouldBan {\n                        durationSection\n                        removeContentSection\n                    }\n                }\n                .navigationTitle(shouldBan ? \"Ban \\(person.name)\" : \"Unban \\(person.name)\")\n                .navigationBarTitleDisplayMode(.inline)\n                .toolbar {\n                    ToolbarItem(placement: .topBarLeading) {\n                        CloseButtonView(ios18Label: .cancel)\n                    }\n                    ToolbarItem(placement: .topBarTrailing) {\n                        Button(\"Send\", icon: .lemmy.send) {\n                            Task { await send() }\n                        }\n                        .glassProminentButtonStyle()\n                    }\n                }\n            }\n        }\n    }\n    \n    var scopeSectionTitle: LocalizedStringResource {\n        if community != nil, appState.firstApi.isAdmin {\n            shouldBan ? \"Ban from...\" : \"Unban from...\"\n        } else {\n            shouldBan ? \"Banning from...\" : \"Unbanning from...\"\n        }\n    }\n    \n    @ViewBuilder\n    var scopeSection: some View {\n        Section {\n            if let instance = appState.firstSession.instance {\n                if let community, appState.firstApi.isAdmin, isBannedFromCommunity == person.bannedFromInstance {\n                    Menu {\n                        Picker(\"Ban Target\", selection: $targetInstance) {\n                            Label(instance).tag(true)\n                            Label(community).tag(false)\n                        }\n                    } label: {\n                        HStack {\n                            targetLabel\n                            Spacer()\n                            Image(icon: .general.dropDown)\n                                .fontWeight(.semibold)\n                                .foregroundStyle(.themedSecondary)\n                        }\n                    }\n                    .buttonStyle(.empty)\n                } else {\n                    targetLabel\n                }\n            }\n        } header: {\n            Text(scopeSectionTitle)\n                .textCase(nil)\n        }\n    }\n    \n    @ViewBuilder\n    var targetLabel: some View {\n        if let selectedTarget {\n            HStack {\n                CircleCroppedImageView(selectedTarget, frame: 24)\n                Text(selectedTarget.name)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var reasonSection: some View {\n        if let selectedTarget {\n            Group {\n                Section {\n                    TextField(\"Reason\", text: $reason, axis: .vertical)\n                        .focused($focusedField, equals: .reason)\n                }\n                Section {\n                    ReasonShortcutView(reason: $reason, rulesTarget: selectedTarget)\n                }\n                .listSectionSpacing(10)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var durationSection: some View {\n        Section {\n            Toggle(\"Permanent\", isOn: $isPermanent)\n                .tint(.themedWarning)\n        }\n        .listSectionSpacing(60)\n        Section(\"Ban Duration\") {\n            HStack {\n                Text(\"Days:\")\n                    .onTapGesture {\n                        focusedField = .days\n                    }\n                TextField(String(\"\"), value: Binding(\n                    get: { days },\n                    set: { days = $0 }\n                ), format: .number)\n                    .keyboardType(.numberPad)\n                    .focused($focusedField, equals: .days)\n            }\n            DatePicker(\n                \"Expires:\",\n                selection: $expiryDate,\n                in: Date.now...,\n                displayedComponents: [.date, .hourAndMinute]\n            )\n            HStack {\n                daysPresetButton(.init(day: 1), value: 1)\n                daysPresetButton(.init(day: 3), value: 3)\n                daysPresetButton(.init(day: 7), value: 7)\n                daysPresetButton(.init(day: 30), value: 30)\n                daysPresetButton(.init(day: 60), value: 60)\n                daysPresetButton(.init(day: 90), value: 90)\n                daysPresetButton(.init(year: 1), value: 365)\n            }\n            .padding(.horizontal, -8)\n        }\n        .opacity(isPermanent ? 0.5 : 1)\n        .disabled(isPermanent)\n    }\n    \n    @ViewBuilder\n    func daysPresetButton(_ date: DateComponents, value: Int) -> some View {\n        Button(dateFormatter.string(for: date) ?? \"\") {\n            days = value\n            hapticManager.play(haptic: .gentleInfo, tier: .low)\n        }\n        .buttonStyle(BanFormButtonStyle(selected: days == value && !isPermanent))\n    }\n    \n    @ViewBuilder\n    var removeContentSection: some View {\n        Section {\n            Toggle(\"Remove Content\", isOn: $removeContent)\n                .tint(.themedWarning)\n        }\n    }\n}\n\nprivate struct BanFormButtonStyle: ButtonStyle {\n    let selected: Bool\n    \n    func makeBody(configuration: Configuration) -> some View {\n        configuration.label\n            .font(.callout)\n            .foregroundStyle(selected ? .themedContrastingLabel : .themedPrimary)\n            .padding(.vertical, 4)\n            .frame(maxWidth: 150)\n            .background(selected ? .themedAccent : .themedGroupedBackground, in: .rect(cornerRadius: 6))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/RegistrationApplicationDenialEditorView.swift",
    "content": "//\n//  RegistrationApplicationDenialEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-14.\n//\n\nimport ComponentViews\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nstruct RegistrationApplicationDenialEditorView: View {\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.dismiss) var dismiss\n    \n    let application: RegistrationApplication\n    \n    @State var reason: String = \"\"\n    @FocusState var reasonFocused: Bool\n    @State var presentationSelection: PresentationDetent = .large\n\n    var body: some View {\n        CollapsibleSheetView(presentationSelection: $presentationSelection, canDismiss: reason.isEmpty) {\n            NavigationStack {\n                Form {\n                    TextField(\"Reason (Optional)\", text: $reason, axis: .vertical)\n                        .focused($reasonFocused)\n                }\n                .scrollDismissesKeyboard(.interactively)\n                .navigationBarTitleDisplayMode(.inline)\n                .navigationTitle(\"Deny Application\")\n                .toolbar {\n                    ToolbarItem(placement: .topBarLeading) {\n                        CloseButtonView(ios18Label: .cancel)\n                    }\n                    ToolbarItem(placement: .topBarTrailing) {\n                        Button(\"Send\", icon: .lemmy.send) {\n                            Task { await send() }\n                        }\n                        .glassProminentButtonStyle()\n                    }\n                }\n            }\n            .onAppear { reasonFocused = true }\n        }\n    }\n    \n    func send() async {\n        let result = await application.deny(reason: reason.isEmpty ? nil : reason).result.get()\n        switch result {\n        case .succeeded:\n            hapticManager.play(haptic: .success, tier: .low)\n            dismiss()\n        case .failed:\n            ToastModel.main.add(.failure())\n        default:\n            break\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Editors/ReportEditorView.swift",
    "content": "//\n//  ReportEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/08/2024.\n//\n\nimport ComponentViews\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ReportEditorView: View {\n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.dismiss) var dismiss\n    \n    let target: any ReportableProviding\n    \n    @State var community: (any ValueProviding<(Community)>)?\n    @State var reason: String = \"\"\n    @FocusState var reasonFocused: Bool\n    @State var presentationSelection: PresentationDetent = .large\n    \n    init(target: any ReportableProviding, community: Community?) {\n        self.target = target\n        \n        if let community {\n            self._community = .init(wrappedValue: RealizedValue(community))\n        } else if let community = (target as? any InteractableProviding)?.community {\n            self._community = .init(wrappedValue: community)\n        } else {\n            self._community = .init(wrappedValue: nil)\n        }\n    }\n\n    var body: some View {\n        CollapsibleSheetView(presentationSelection: $presentationSelection, canDismiss: reason.isEmpty) {\n            NavigationStack {\n                Form {\n                    TextField(\"Reason\", text: $reason, axis: .vertical)\n                        .focused($reasonFocused)\n                    Section {\n                        ReasonShortcutView(reason: $reason)\n                    }\n                    // ExpectedView causes rendering issues here\n                    if let community = community?.value {\n                        RulesListView(model: community, reason: $reason)\n                    }\n                    if let instance = appState.firstSession.instance {\n                        RulesListView(model: instance, reason: $reason)\n                    }\n                }\n                .scrollDismissesKeyboard(.interactively)\n                .navigationBarTitleDisplayMode(.inline)\n                .toolbar {\n                    ToolbarItem(placement: .topBarLeading) {\n                        CloseButtonView(ios18Label: .cancel)\n                    }\n                    ToolbarItem(placement: .topBarTrailing) {\n                        Button(\"Send\", icon: .lemmy.send) {\n                            Task {\n                                await send()\n                            }\n                        }\n                        .glassProminentButtonStyle()\n                        .disabled(reason.isEmpty)\n                    }\n                }\n            }\n            .onAppear { reasonFocused = true }\n        }\n    }\n    \n    func send() async {\n        do {\n            try await target.report(reason: reason)\n            hapticManager.play(haptic: .success, tier: .low)\n            dismiss()\n        } catch {\n            handleError(error)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/ExternalApiInfoView.swift",
    "content": "//\n//  ExternalApiInfoView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/06/2024.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ExternalApiInfoView: View {\n    @Environment(AppState.self) private var appState\n    \n    @State private var isLoading: Bool = true\n    \n    @State private var internalFederationStatus: FederationStatus?\n    @State private var externalFederationStatus: FederationStatus?\n    @State private var externalInstance: Instance?\n    \n    /// The ``ApiClient`` of the model being inspected.\n    let fallbackApi: ApiClient\n    /// The local ``ApiClient`` of the model, created using `entityHost`.\n    let entityLocalApi: ApiClient\n    \n    init(api: ApiClient, actorId: ActorIdentifier) {\n        self.fallbackApi = api\n        self.entityLocalApi = .getApiClient(url: actorId.hostUrl, username: nil)\n    }\n    \n    var body: some View {\n        VStack {\n            if isLoading {\n                Text(\"Diagnosing...\")\n                    .foregroundStyle(.secondary)\n            } else {\n                ScrollView {\n                    content\n                }\n            }\n        }\n        .frame(maxWidth: .infinity, maxHeight: .infinity)\n        .animation(.easeOut(duration: 0.2), value: isLoading)\n        .background(.themedGroupedBackground)\n        .presentationBackground(.themedGroupedBackground)\n        .task { await loadData() }\n        .presentationBackgroundInteraction(.enabled)\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        VStack(spacing: 16) {\n            box(spacing: 16) {\n                if internalFederationStatus?.isAllowed ?? false, externalFederationStatus?.isAllowed ?? false {\n                    Text(\n                        // swiftlint:disable:next line_length\n                        \"Your instance and **\\(entityLocalApi.host)** federate, but the content could not be loaded. It may not have federated yet, or your instance may have purged it.\"\n                    )\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                } else {\n                    avatars\n                        .padding(.horizontal, 16)\n                    Text(text)\n                        .multilineTextAlignment(.center)\n                        .padding(.horizontal, 16)\n                }\n            }\n            box(alignment: .leading) {\n                Text(\"This content will be loaded from **\\(fallbackApi.host)** instead.\")\n                    .frame(maxWidth: .infinity, alignment: .leading)\n                    .padding(.horizontal, Constants.main.standardSpacing)\n            }\n            box(alignment: .leading, spacing: 6) {\n                Text(\"What is Federation?\")\n                    .font(.title2)\n                    .fontWeight(.bold)\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                \n                Text(\n                    String(\n                        localized: \"federation.explanation\",\n                        // swiftlint:disable:next line_length\n                        defaultValue: \"Lemmy instances talk to each other so that content can be shared across sites. This is called \\\"federation\\\". Instance administrators can choose which other instances they would like their instance to federate with. Some instances federate with all but a curated \\\"block-list\\\" of other instances; other instances might only federate with instances on an \\\"allow-list\\\".\"\n                    )\n                )\n                .padding(.horizontal, Constants.main.standardSpacing)\n            }\n        }\n        .frame(maxWidth: .infinity)\n        .padding(16)\n    }\n    \n    @ViewBuilder func box(\n        alignment: HorizontalAlignment = .center,\n        spacing: CGFloat = 16,\n        @ViewBuilder content: () -> some View\n    ) -> some View {\n        VStack(alignment: alignment, spacing: spacing) {\n            content()\n        }\n        .padding(.vertical, 16)\n        .frame(maxWidth: .infinity)\n        .background(\n            .themedSecondaryGroupedBackground,\n            in: .rect(cornerRadius: Constants.main.mediumItemCornerRadius)\n        )\n    }\n    \n    @ViewBuilder\n    var avatars: some View {\n        Line()\n            .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, dash: [5]))\n            .frame(height: 2)\n            .foregroundStyle(.themedTertiary)\n            .frame(width: 150, height: 48)\n            .padding(.horizontal)\n            .overlay {\n                HStack {\n                    CircleCroppedImageView(appState.firstSession.instance, frame: 48)\n                    Image(icon: .general.failure)\n                        .bold()\n                        .foregroundStyle(.red)\n                        .imageScale(.large)\n                        .frame(maxWidth: .infinity)\n                    CircleCroppedImageView(externalInstance, frame: 48)\n                }\n            }\n    }\n    \n    var text: LocalizedStringKey {\n        let externalHost = entityLocalApi.host\n        let internalHost = appState.firstApi.host\n        switch (externalFederationStatus?.isAllowed ?? false, internalFederationStatus?.isAllowed ?? false) {\n        case (false, false):\n            return \"**\\(internalHost)** and **\\(externalHost)** chose to defederate from one another.\"\n        case (false, true):\n            if externalFederationStatus?.isExplicit ?? false {\n                return \"**\\(externalHost)** chose to defederate from your instance, **\\(internalHost)**.\"\n            } else {\n                return \"**\\(externalHost)** hasn't chosen to federate with your instance, **\\(internalHost)**.\"\n            }\n        case (true, false):\n            if internalFederationStatus?.isExplicit ?? false {\n                return \"Your instance, **\\(internalHost)**, chose to defederate from **\\(externalHost)**.\"\n            } else {\n                return \"Your instance, **\\(internalHost)**, hasn't chosen to federate with **\\(externalHost)**.\"\n            }\n        case (true, true):\n            return \"Unknown\"\n        }\n    }\n    \n    @MainActor\n    func loadData() async {\n        let externalApi = entityLocalApi\n        let internalApi = appState.firstApi\n        \n        do {\n            async let externalFederationStatus = await externalApi.federatedWith(with: internalApi.baseUrl)\n            async let internalFederationStatus = await internalApi.federatedWith(with: externalApi.baseUrl)\n            async let externalInstance = await externalApi.getMyInstance()\n            \n            self.externalFederationStatus = try await externalFederationStatus\n            self.internalFederationStatus = try await internalFederationStatus\n            self.externalInstance = try await externalInstance\n        } catch {\n            handleError(error, silent: true)\n        }\n        \n        isLoading = false\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/ImageViewer+Views.swift",
    "content": "//\n//  ImageViewer+Views.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-01-18.\n//\n\nimport SwiftUI\nimport Icons\n\n// swiftlint:disable file_length\n\nextension ImageViewer {\n    struct ControlTranslationEffect: GeometryEffect {\n        var offset: CGFloat\n        var isDismissing: Bool\n\n        var animatableData: CGFloat {\n            get { isDismissing ? 0 : offset }\n            set { offset = newValue }\n        }\n\n        func effectValue(size: CGSize) -> ProjectionTransform {\n            return ProjectionTransform(.init(translationX: 0, y: offset))\n        }\n    }\n\n    @ViewBuilder\n    var controlOverlay: some View {\n        VStack {\n            topControlBar\n                .modifier(ControlTranslationEffect(offset: -controlOffset, isDismissing: isDismissing))\n            Spacer()\n            bottomControlBar\n                .modifier(ControlTranslationEffect(offset: controlOffset, isDismissing: isDismissing))\n        }\n        .font(.title2)\n        .fontWeight(.light)\n        .foregroundStyle(.white)\n        .labelStyle(.iconOnly)\n        .opacity(controlOpacity)\n    }\n    \n    // MARK: Top control bar\n    \n    @ViewBuilder\n    var topControlBar: some View {\n        HStack(alignment: .top) {\n            Spacer()\n            if developerMode {\n                devTools\n            }\n            if showCloseButton {\n                closeButton\n                    .padding(.trailing, Constants.main.standardSpacing)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var closeButton: some View {\n        Button {\n            fadeDismiss()\n        } label: {\n            if #available(iOS 26, *) {\n                closeButtonContent\n                    .glassEffect(.regular.interactive())\n            } else {\n                closeButtonContent\n                    .background(.ultraThinMaterial, in: .circle)\n            }\n        }\n        .contentShape(.rect)\n        .environment(\\.colorScheme, .dark)\n    }\n    \n    @ViewBuilder\n    var devTools: some View {\n        Group {\n            if !devToolsShown {\n                Button {\n                    withAnimation {\n                        devToolsShown = true\n                    }\n                } label: {\n                    if #available(iOS 26, *) {\n                        devToolsButtonContent\n                            .glassEffect(.regular.interactive())\n                    } else {\n                        devToolsButtonContent\n                            .background(.ultraThinMaterial, in: .circle)\n                    }\n                }\n                .contentShape(.rect)\n                .environment(\\.colorScheme, .dark)\n            } else {\n                Group {\n                    if #available(iOS 26, *) {\n                        devToolsContent\n                            .glassEffect(.regular.interactive(), in: .rect(cornerRadius: Constants.main.standardSpacing))\n                    } else {\n                        devToolsContent\n                            .background(.ultraThinMaterial, in: .rect(cornerRadius: Constants.main.standardSpacing))\n                    }\n                }\n                .onTapGesture {\n                    withAnimation {\n                        devToolsShown = false\n                    }\n                }\n            }\n        }\n        .environment(\\.colorScheme, .dark)\n    }\n    \n    // MARK: Bottom control bar\n    \n    @ViewBuilder\n    var bottomControlBar: some View {\n        VStack(spacing: 0) {\n            if controlState.animationAvailable {\n                playbackBar\n            }\n            \n            ZStack(alignment: .bottom) {\n                if controlState.animationAvailable {\n                    playButton\n                        .frame(maxWidth: .infinity, alignment: .leading)\n                }\n                \n                Group {\n                    if #available(iOS 26, *) {\n                        bottomControlBarContent\n                            .glassEffect(.regular.interactive())\n                    } else {\n                        bottomControlBarContent\n                            .background {\n                                Capsule().fill(.ultraThinMaterial)\n                            }\n                    }\n                }\n                .frame(maxWidth: .infinity, alignment: .center)\n                \n                if controlState.audioAvailable {\n                    muteButton\n                        .frame(maxWidth: .infinity, alignment: .trailing)\n                }\n            }\n        }\n        .environment(\\.colorScheme, .dark)\n    }\n    \n    @ViewBuilder\n    var playbackBar: some View {\n        VStack(spacing: Constants.main.halfSpacing) {\n            if let readouts = controlState.playbackReadouts {\n                HStack {\n                    Text(readouts.position)\n                    Spacer()\n                    Text(readouts.duration)\n                }\n                .font(.footnote)\n                .fontWeight(.semibold)\n                .shadow(radius: 2)\n            }\n            \n            playbackBarBaseCapsule\n                .frame(maxWidth: .infinity)\n                .frame(height: 10)\n                .overlay {\n                    GeometryReader { geo in\n                        let width = geo.size.width - 10 // prevent circle going past end of capsule\n                        Circle()\n                            .fill(.white)\n                            .frame(width: 6, height: 6)\n                            .padding(2)\n                            .offset(x: (controlState.scrubTarget ?? controlState.playbackPosition) * width)\n                            .onAppear {\n                                // set playbackBarHitbox to be a bit thicker than the real hitbox\n                                let realHitbox = geo.frame(in: .global)\n                                playbackBarHitbox = .init(\n                                    x: realHitbox.minX,\n                                    y: realHitbox.maxY - 80,\n                                    width: realHitbox.width,\n                                    height: 100\n                                )\n                            }\n                    }\n                }\n                .environment(\\.colorScheme, .dark)\n        }\n        .padding(.horizontal, Constants.main.standardSpacing)\n        .allowsHitTesting(false)\n    }\n    \n    @ViewBuilder\n    var playButton: some View {\n        Button {\n            controlState.animating.toggle()\n        } label: {\n            Group {\n                if #available(iOS 26, *) {\n                    playButtonContent\n                        .glassEffect(.regular.interactive())\n                } else {\n                    playButtonContent\n                        .background(.ultraThinMaterial, in: .circle)\n                }\n            }\n            .padding(.leading, Constants.main.standardSpacing)\n            .padding([.top, .trailing], Constants.main.doubleSpacing)\n            .contentShape(.rect)\n        }\n    }\n    \n    @ViewBuilder\n    var saveButton: some View {\n        Button {\n            Task { await saveMedia(url: url) }\n        } label: {\n            Label(\"Save\", icon: .general.import)\n                .padding(Constants.main.standardSpacing)\n                .contentShape(.rect)\n        }\n        .offset(y: -2)\n    }\n    \n    @ViewBuilder\n    var shareButton: some View {\n        Button {\n            Task { await shareImage(url: url, navigation: navigation) }\n        } label: {\n            Label(\"Share\", icon: .general.share)\n                .padding(Constants.main.standardSpacing)\n                .contentShape(.rect)\n        }\n        .offset(y: -2)\n    }\n    \n    @ViewBuilder\n    var quickLookButton: some View {\n        Button {\n            Task { await showQuickLook(url: url) }\n        } label: {\n            Label(\"Quick Look\", icon: .general.menu)\n                .symbolVariant(.circle)\n                .padding(Constants.main.standardSpacing)\n                .contentShape(.rect)\n        }\n    }\n    \n    @ViewBuilder\n    var muteButton: some View {\n        Button {\n            controlState.muted.toggle()\n        } label: {\n            Group {\n                if #available(iOS 26, *) {\n                    muteButtonContent\n                        .glassEffect(.regular.interactive())\n                } else {\n                    muteButtonContent\n                        .background(.ultraThinMaterial, in: .circle)\n                }\n            }\n            .padding(.trailing, Constants.main.standardSpacing)\n            .padding([.top, .leading], Constants.main.doubleSpacing)\n            .contentShape(.rect)\n        }\n    }\n    \n    // MARK: Zoom and Scale\n    \n    @ViewBuilder\n    var scaleDisplay: some View {\n        Group {\n            if #available(iOS 26, *) {\n                scaleDisplayContent\n                    .glassEffect()\n            } else {\n                scaleDisplayContent\n                    .background {\n                        Capsule().fill(.ultraThinMaterial)\n                    }\n            }\n        }\n        .environment(\\.colorScheme, .dark)\n        .padding(.leading, Constants.main.standardSpacing)\n        .opacity(scaleDisplayShown ? 1 : 0)\n    }\n    \n    @ViewBuilder\n    func buttonLabel(text: LocalizedStringResource, icon: Icon, frameSize: CGFloat, padding: CGFloat) -> some View {\n        Label {\n            Text(text)\n        } icon: {\n            Image(icon: icon)\n                .resizable()\n                .scaledToFit()\n                .frame(width: frameSize, height: frameSize)\n                .padding(padding)\n        }\n        .labelStyle(.iconOnly)\n    }\n    \n    @ViewBuilder\n    func videoStateButtonLabel(\n        isOn: Bool,\n        text: (on: LocalizedStringResource, off: LocalizedStringResource),\n        icons: (on: Icon, off: Icon)) -> some View {\n        Label {\n            Text(isOn ? text.on : text.off)\n        } icon: {\n            Image(icon: isOn ? icons.on : icons.off)\n                .symbolVariant(.fill)\n                .scaledToFit()\n                .frame(width: 22, height: 22)\n                .contentTransition(.symbolEffect(.replace, options: .speed(2)))\n                .padding(Constants.main.standardSpacing + 4) // +4 to match .title2 implicit padding plus offset\n        }\n        .labelStyle(.iconOnly)\n    }\n    \n    // MARK: Platform Compatibility\n    // TODO: iOS 18 deprecation remove\n    \n    @ViewBuilder\n    var closeButtonContent: some View {\n        buttonLabel(text: \"Close\", icon: .general.close, frameSize: 18, padding: Constants.main.standardSpacing + 6)\n    }\n    \n    @ViewBuilder\n    var devToolsButtonContent: some View {\n        buttonLabel(\n            text: \"Toggle Developer Tools\",\n            icon: .settings.developerMode,\n            frameSize: 22,\n            padding: Constants.main.standardSpacing + 4\n        )\n    }\n    \n    @ViewBuilder\n    var devToolsContent: some View {\n        VStack(alignment: .leading, spacing: Constants.main.halfSpacing) {\n            let imageType: String = url.proxyAwarePathExtension?.lowercased() ?? \"Unknown\"\n            Text(verbatim: \"Media Type: \\(imageType) \")\n            if let duration = controlState.duration {\n                Text(verbatim: \"Duration: \\(String(format: \"%.4fs\", duration))\")\n                    .monospacedDigit()\n            } else {\n                Text(verbatim: \"Duration: None\")\n            }\n            Text(verbatim: \"Playback Position: \\(String(format: \"%.4f\", controlState.playbackPosition))\")\n                .monospacedDigit()\n            if let target = controlState.scrubTarget {\n                Text(verbatim: \"Scrub Target: \\(String(format: \"%.4f\", target))\")\n                    .monospacedDigit()\n            } else {\n                Text(verbatim: \"Scrub Target: None\")\n            }\n            Text(verbatim: \"Scrub Rate: \\(String(format: \"%.4f\", scrubRate))\")\n                .monospacedDigit()\n        }\n        .padding(Constants.main.standardSpacing)\n        .contentShape(.rect)\n        .foregroundStyle(.white)\n        .font(.footnote)\n    }\n    \n    @ViewBuilder\n    var playbackBarBaseCapsule: some View {\n        if #available(iOS 26, *) {\n            Color.clear.contentShape(.rect)\n                .glassEffect()\n        } else {\n            Capsule()\n                .fill(.ultraThinMaterial)\n        }\n    }\n    \n    @ViewBuilder\n    var bottomControlBarContent: some View {\n        HStack {\n            saveButton\n            shareButton\n            quickLookButton\n        }\n        .padding(.horizontal, Constants.main.halfSpacing)\n    }\n    \n    @ViewBuilder\n    var playButtonContent: some View {\n        videoStateButtonLabel(\n            isOn: controlState.animating,\n            text: (on: \"Pause\", off: \"Play\"),\n            icons: (on: .general.pause, off: .general.play))\n    }\n    \n    @ViewBuilder\n    var muteButtonContent: some View {\n        videoStateButtonLabel(\n            isOn: controlState.muted,\n            text: (on: \"Mute\", off: \"Unmute\"),\n            icons: (on: .general.mute, off: .general.unmute))\n    }\n    \n    @ViewBuilder\n    var scaleDisplayContent: some View {\n        Text(String(format: \"%.1fx\", scaleDisplayValue))\n            .foregroundStyle(.white)\n            .padding(Constants.main.standardSpacing)\n            .padding(.horizontal, Constants.main.halfSpacing)\n    }\n}\n// swiftlint:enable file_length\n"
  },
  {
    "path": "Mlem/App/Views/Pages/ImageViewer.swift",
    "content": "//\n//  ImageViewer.swift\n//  Mlem\n//\n//  Created by Sjmarf on 13/06/2024.\n//\n\nimport SwiftUI\nimport Media\n\nstruct ImageViewer: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.dismiss) var dismiss\n    @Environment(\\.colorScheme) var colorScheme\n    @Setting(\\.dev_developerMode) var developerMode\n    @Setting(\\.a11y_zoomSliderLocation) var zoomSliderLocation\n    \n    let url: URL\n    \n    let duration: CGFloat = 0.25\n    let maxControlOffset: CGFloat = 50\n    let screenHeight: CGFloat = UIScreen.main.bounds.height\n    \n    @State var controlState: MediaControlState = .init(\n        blurred: false,\n        animating: true,\n        muted: Settings.get(\\.behavior_muteVideos),\n        scrubbingAvailable: true\n    )\n\n    @Setting(\\.imageViewer_showControls) var showControls\n    @Setting(\\.imageViewer_showCloseButton) var showCloseButton\n    @Setting(\\.imageViewer_showZoomIndicator) var showZoomIndicator\n    @Setting(\\.imageViewer_dismissThreshold) var dismissThreshold\n    \n    /// Current scale of the zoomable image\n    @State var zoomScale: CGFloat = 1.0\n    \n    /// Offset of the zoomable image\n    @State var zoomOffset: CGSize = .zero\n    \n    /// True when the scale indicator should be visible, false otherwise\n    @State var scaleDisplayShown: Bool = false\n    \n    /// True when the current drag gesture is a scrub, false when dismiss, nil when no gesture\n    @State var dragIsScrub: Bool?\n    \n    /// controlState.playbackPosition when current scrub segment began\n    @State var scrubStartedPlaybackPosition: CGFloat?\n    \n    /// controlState.playbackPosition when current scrub segment began\n    @State var scrubSegmentOffset: CGFloat = 0\n    \n    /// Current scrubbing rate\n    @State var scrubRate: CGFloat = 1\n    \n    /// Hitbox of the playback bar\n    @State var playbackBarHitbox: CGRect?\n    \n    /// True when the image is zoomed in, false otherwise\n    @State var isZoomed: Bool = false\n    \n    /// True when dimissal is in progress, false otherwise\n    @State var isDismissing: Bool = false\n    \n    /// Vertical offset of the viewer\n    @State var offset: CGFloat = 0\n    \n    /// Opacity of the viewer\n    @State var opacity: CGFloat = 0\n    \n    /// Vertical offset for the control overlay\n    @State var controlOffset: CGFloat = 0\n    \n    /// Opacity for the control overlay\n    @State var controlOpacity: CGFloat = 1\n    \n    /// When true, enables tapping to show/hide controls\n    @State var enableControlTap: Bool = true\n    \n    @State var quickLookUrl: URL?\n    \n    /// Value to show in the top leading scale display (either scrub rate or zoom depending)\n    @State var scaleDisplayValue: CGFloat = 1\n    \n    @State var devToolsShown: Bool = false\n    \n    /// Whether the controls are currently visible\n    var controlsShown: Bool { controlOpacity > 0 }\n    \n    init(url: URL) {\n        var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!\n        components.queryItems = components.queryItems?.filter { $0.name != \"thumbnail\" }\n        self.controlOpacity = Settings.get(\\.imageViewer_showControls) == .immediately ? 1 : 0\n        self.url = components.url!\n    }\n    \n    var body: some View {\n        ZoomableImageView(\n            url: url,\n            controlState: $controlState,\n            scale: $zoomScale,\n            offset: $zoomOffset,\n            customDragMoved: dragMoved,\n            customDragEnded: dragEnded\n        ) {\n            if enableControlTap, showControls != .never {\n                if controlsShown {\n                    hideControls()\n                } else {\n                    showControls()\n                }\n            }\n        }\n        .offset(y: offset)\n        .background(.black)\n        .overlay(controlOverlay)\n        .overlay(alignment: .topLeading) {\n            if showZoomIndicator {\n                scaleDisplay\n            }\n        }\n        .opacity(opacity)\n        .onChange(of: isZoomed) {\n            if isZoomed {\n                hideControls(withSlide: true)\n            } else if showControls == .immediately {\n                showControls(withSlide: true)\n            }\n        }\n        .onAppear {\n            animateOpacityUpdate(1.0)\n        }\n        .onChange(of: scrubRate) {\n            if dragIsScrub ?? false { // don't update value if not currently scrubbing\n                scaleDisplayValue = scrubRate\n            }\n        }\n        .onChange(of: zoomScale) {\n            scaleDisplayValue = zoomScale\n            isZoomed = zoomScale != 1.0\n        }\n        .onChange(of: scaleDisplayValue) {\n            if !scaleDisplayShown {\n                withAnimation(.easeIn(duration: 0.1)) {\n                    scaleDisplayShown = true\n                }\n            }\n            let oldScale: CGFloat = scaleDisplayValue\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                if scaleDisplayValue == oldScale {\n                    withAnimation {\n                        scaleDisplayShown = false\n                    }\n                }\n            }\n        }\n        .quickLookPreview($quickLookUrl)\n        .background(ClearBackgroundView())\n        .statusBarHidden(!isDismissing)\n    }\n    \n    func dragMoved(value: BridgeDragValue) {\n        guard !isZoomed else {\n            return\n        }\n        \n        let dragIsScrub = dragIsScrub ?? (abs(value.velocity.height) < abs(value.velocity.width))\n        self.dragIsScrub = dragIsScrub\n        \n        if dragIsScrub {\n            if controlState.animationAvailable, controlState.enableAnimation {\n                handleScrubUpdate(value)\n            }\n        } else if !isDismissing {\n            handleOffsetUpdate(value.translation.height)\n        }\n    }\n    \n    func dragEnded() {\n        guard let scrubbing = dragIsScrub else {\n            assertionFailure(\"dragGesture ended but dragIsScrub not defined\")\n            return\n        }\n        dragIsScrub = nil\n        \n        if scrubbing {\n            // scrub ended: reset scrubbing and re-enable control tap\n            scrubRate = 1\n            scrubStartedPlaybackPosition = nil\n            scrubSegmentOffset = 0\n            controlState.scrubTarget = nil\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                enableControlTap = true\n            }\n        } else {\n            // dismiss swipe ended: choose whether to dismiss or reset\n            if abs(offset) > CGFloat(dismissThreshold) * 10 {\n                swipeDismiss(finalOffset: offset > 0 ? screenHeight : -screenHeight)\n            } else {\n                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                    enableControlTap = true\n                }\n                animateOffsetUpdate(0)\n            }\n        }\n    }\n    \n    func fadeDismiss() {\n        isDismissing = true\n        animateOpacityUpdate(0) {\n            withoutAnimation {\n                dismiss()\n            }\n        }\n    }\n    \n    private func swipeDismiss(finalOffset: CGFloat = UIScreen.main.bounds.height) {\n        isDismissing = true\n        animateOffsetUpdate(finalOffset) {\n            withoutAnimation {\n                dismiss()\n            }\n        }\n    }\n    \n    func hideControls(withSlide: Bool = false) {\n        withAnimation(.easeOut(duration: duration)) {\n            if withSlide {\n                controlOffset = maxControlOffset\n            }\n            controlOpacity = 0\n        }\n    }\n    \n    /// Returns controls to a visible state\n    func showControls(withSlide: Bool = false) {\n        guard !controlsShown else { return }\n        \n        controlOffset = withSlide ? maxControlOffset : 0\n        \n        withAnimation(.easeIn(duration: duration)) {\n            controlOpacity = 1\n            if withSlide {\n                controlOffset = 0\n            }\n        }\n    }\n    \n    private func animateOpacityUpdate(_ newOpacity: CGFloat, callback: (() -> Void)? = nil) {\n        withAnimation(.easeOut(duration: duration)) {\n            opacity = newOpacity\n        }\n        if let callback {\n            DispatchQueue.main.asyncAfter(deadline: .now() + duration) {\n                callback()\n            }\n        }\n    }\n    \n    /// Sets the offsets to the given value with animation. If a callback is given, calls it when the animation completes.\n    /// - Parameters:\n    ///   - newOffset: value to update offsets to\n    ///   - callback: function to call when animation completes\n    private func animateOffsetUpdate(_ newOffset: CGFloat, callback: (() -> Void)? = nil) {\n        withAnimation(.easeOut(duration: duration)) {\n            handleOffsetUpdate(newOffset)\n        }\n        if let callback {\n            DispatchQueue.main.asyncAfter(deadline: .now() + duration) {\n                callback()\n            }\n        }\n    }\n    \n    /// Updates offset, controlOffset, and opacity to match the given raw offset˜\n    /// - Parameter newOffset: raw offset to update for\n    private func handleOffsetUpdate(_ newOffset: CGFloat) {\n        let absOffset = abs(newOffset)\n        offset = newOffset\n        if controlsShown {\n            controlOffset = absOffset / 3\n            controlOpacity = 1.0 - (controlOffset / maxControlOffset)\n        }\n        opacity = 1.0 - (absOffset / screenHeight)\n    }\n    \n    /// Responds to scrub updates\n    /// - Parameter value: latest scrub gesture value\n    private func handleScrubUpdate(_ value: BridgeDragValue) {\n        showControls()\n        \n        let onPlaybackBar: Bool = playbackBarHitbox?.contains(value.startLocation) ?? false\n        \n        // Track playback position when current scrub segment started to offset from.\n        // If the scrub started on playback bar, we want to snap to the start location, so we set scrubStartedPlaybackPosition\n        // to the value corresponding to the scrub start position\n        if scrubStartedPlaybackPosition == nil {\n            scrubStartedPlaybackPosition = onPlaybackBar ?\n                value.startLocation.x / UIScreen.main.bounds.width :\n                controlState.playbackPosition\n        }\n        \n        // disable variable scrub rate if scrubbing playback bar\n        if !onPlaybackBar {\n            // scrub rate is controlled by the height of the scrub gesture.\n            // Every 50px increases/decreases scrub rate by 2x to a max of 8x; update in increments of 10px\n            let heightStep: CGFloat = value.translation.height.stepped(by: 10) / 50\n            let newScrubRate: CGFloat = (1 / pow(2, heightStep)).bounded(lower: 0.125, upper: 8)\n            if newScrubRate != scrubRate {\n                // when the scrub rate changes, compute future scrub targets as if the translation started at the current point and scrubTarget\n                scrubStartedPlaybackPosition = controlState.scrubTarget ?? controlState.playbackPosition\n                scrubSegmentOffset = value.translation.width\n                scrubRate = newScrubRate\n            }\n        }\n        \n        guard let scrubStartedPlaybackPosition else {\n            assertionFailure(\"drag is scrub but scrubStartedPlaybackPosition is nil\")\n            return\n        }\n        \n        // compute x translation since scrub segment began and adjust by scrub rate\n        let scrubSegmentTranslation = (value.translation.width - scrubSegmentOffset) * scrubRate\n        // convert translation to a percentage of scrub area\n        let scrubTargetDelta = scrubSegmentTranslation / UIScreen.main.bounds.width\n        let newScrubTarget = (scrubStartedPlaybackPosition + scrubTargetDelta).bounded(lower: 0, upper: 1)\n        controlState.scrubTarget = newScrubTarget\n    }\n    \n    func showQuickLook(url: URL) async {\n        if let fileUrl = await downloadImageToFileSystem(url: url) {\n            quickLookUrl = fileUrl\n        }\n    }\n}\n\n// https://stackoverflow.com/a/75037657\n// .presentationBackground doesn't behave properly on iOS 17, but this does\n// TODO: iOS 17 deprecation: remove this and replace usage with .presentationBackground\nprivate struct ClearBackgroundView: UIViewRepresentable {\n    func makeUIView(context: Context) -> UIView {\n        InnerView()\n    }\n    \n    func updateUIView(_ uiView: UIView, context: Context) {}\n    \n    private class InnerView: UIView {\n        override func didMoveToWindow() {\n            super.didMoveToWindow()\n            \n            superview?.superview?.backgroundColor = .clear\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/Fediseer.swift",
    "content": "//\n//  Fediseer.swift\n//  Mlem\n//\n//  Created by Sjmarf on 03/02/2024.\n//\n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\n// https://fediseer.com/api/v1/whitelist/lemmy.world\n\nstruct FediseerData: Hashable, Equatable {\n    var instance: FediseerInstance\n    var endorsements: [FediseerEndorsement]?\n    var hesitations: [FediseerHesitation]?\n    var censures: [FediseerCensure]?\n    \n    var topEndorsements: [FediseerEndorsement] {\n        if var endorsements {\n            endorsements = endorsements.sorted { $0.reason != nil && $1.reason == nil }\n            return endorsements\n        }\n        return []\n    }\n    \n    func opinions(ofType type: FediseerOpinionType) -> [any FediseerOpinion] {\n        switch type {\n        case .endorsement:\n            endorsements ?? []\n        case .hesitation:\n            hesitations ?? []\n        case .censure:\n            censures ?? []\n        }\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(instance.domain)\n    }\n}\n\nstruct FediseerInstance: Codable, Equatable {\n    let id: Int\n    let domain: String\n    // let software: String\n    let claimed: Int\n    let approvals: Int // This is the number of endorsements given\n    let endorsements: Int // This is the number of endorsements received\n    let guarantor: String?\n    \n    // Fediseer lets instances admins self-report these values\n    let sysadmins: Int?\n    let moderators: Int?\n}\n\nstruct FediseerEndorsements: Codable {\n    var instances: [FediseerEndorsement] = .init()\n}\n\nstruct FediseerHesitations: Codable {\n    var instances: [FediseerHesitation] = .init()\n}\n\nstruct FediseerCensures: Codable {\n    var instances: [FediseerCensure] = .init()\n}\n\nenum FediseerOpinionType: CaseIterable, Identifiable {\n    case endorsement, hesitation, censure\n    \n    var id: FediseerOpinionType { self }\n    \n    var label: String {\n        switch self {\n        case .endorsement: .init(localized: \"Endorsements\")\n        case .hesitation: .init(localized: \"Hesitations\")\n        case .censure: .init(localized: \"Censures\")\n        }\n    }\n}\n\nprotocol FediseerOpinion {\n    var domain: String { get }\n    var reason: String? { get }\n    var evidence: String? { get }\n    \n    static var icon: Icon { get }\n    static var color: ThemedColor { get }\n}\n\nextension FediseerOpinion {\n    var instanceStub: InstanceStub? {\n        guard let url = URL(string: \"https://\\(domain)\") else { return nil }\n        return .init(api: .getApiClient(url: url, username: nil), actorId: .instance(host: domain))\n    }\n    \n    var formattedReason: String? {\n        if let reason {\n            return \"- \\(reason.split(separator: \",\").joined(separator: \"\\n- \"))\"\n        }\n        return nil\n    }\n}\n\nstruct FediseerEndorsement: Codable {\n    let domain: String\n    let endorsementReasons: [String]?\n}\n\nextension FediseerEndorsement: FediseerOpinion, Equatable {\n    static var icon: Icon = .fediseer.endorsement\n    static var color: ThemedColor { .themedFediseerEndorsement }\n    \n    var reason: String? { endorsementReasons?.first }\n    var evidence: String? { nil }\n}\n\nstruct FediseerHesitation: Codable {\n    let domain: String\n    let hesitationReasons: [String]?\n    let hesitationEvidence: [String]?\n}\n\nextension FediseerHesitation: FediseerOpinion, Equatable {\n    static var icon: Icon = .fediseer.hesitation\n    static var color: ThemedColor { .themedFediseerHesitation }\n    \n    var reason: String? { hesitationReasons?.first }\n    var evidence: String? { hesitationEvidence?.first }\n}\n\nstruct FediseerCensure: Codable {\n    let domain: String\n    let censureReasons: [String]?\n    let censureEvidence: [String]?\n}\n\nextension FediseerCensure: FediseerOpinion, Equatable {\n    static var icon: Icon = .fediseer.censure\n    static var color: ThemedColor { .themedFediseerCensure }\n    \n    var reason: String? { censureReasons?.first }\n    var evidence: String? { censureEvidence?.first }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/FediseerInfoView.swift",
    "content": "//\n//  FediseerInfoView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 04/02/2024.\n//\n\nimport ComponentViews\nimport Icons\nimport SwiftUI\n\n// swiftlint:disable line_length\nstruct FediseerInfoView: View {\n    var body: some View {\n        FancyScrollView {\n            VStack(alignment: .leading) {\n                subHeading(\"The Fediseer\", icon: .fediseer.fediseer, color: .indigo)\n                Text(\"The Fediseer is a service that instance administrators use to identify spam instances and express their approval or disapproval of other instances.\")\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                subHeading(\"Guarantees\", icon: .fediseer.guarantee, color: .green)\n                Text(\"If an instance is \\\"guaranteed\\\", it is known as definitely not spam. Unguaranteed instances are not necessarily spam; rather, it is unknown whether a non-guaranteed instance is spam or not.\\n\\nAn instance can be guaranteed by any other guaranteed instance. This forms a chain of guaranteed instances known as the \\\"Chain of Trust\\\". The Chain of Trust starts at the Fediseer itself, which guarantees several of the largest instances.\\n\\nA guarantee can be revoked by the guarantor at any time. If an instance's guarantee is revoked, it returns to a \\\"not guaranteed\\\" state along with any instances it guarantees.\\n\\nOnce an instance has been guaranteed, it is able to express its approval or disapproval of other instances using endorsements, hesitations and censures.\")\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                subHeading(\"Endorsements\", icon: .fediseer.endorsement, color: .teal)\n                Text(\"An endorsement signifies that an instance approves of another instance. It is completely subjective, and a reason does not have to be given.\")\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                subHeading(\"Censures\", icon: .fediseer.censure, color: .red)\n                Text(\"A censure signifies that an instance disapproves of another instance. Like an endorsement, it is completely subjective and a reason does not have to be given.\")\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                subHeading(\"Hesitations\", icon: .fediseer.hesitation, color: .yellow)\n                Text(\"A hesitation signifies that an instance mistrusts another instance. It is a milder version of a censure.\")\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                Divider()\n                    .padding(.top, 20)\n                linkButton(\n                    \"Fediseer FAQ\",\n                    systemImage: \"questionmark.circle.fill\",\n                    destination: URL(string: \"https://fediseer.com/faq/eng\")!\n                )\n                .padding(.bottom, 50)\n            }\n            .frame(maxWidth: .infinity)\n        }\n        .toolbar {\n            CloseButtonToolbarItem()\n        }\n    }\n    \n    @ViewBuilder\n    func subHeading(_ title: LocalizedStringResource, icon: Icon, color: Color) -> some View {\n        VStack(alignment: .leading, spacing: 5) {\n            HStack {\n                Image(icon: icon)\n                    .foregroundStyle(color)\n                    .symbolVariant(.fill)\n                Text(title)\n                    .fontWeight(.semibold)\n            }\n            .font(.title2)\n            .padding(.horizontal, Constants.main.standardSpacing)\n            Divider()\n        }\n        .padding(.top, 20)\n    }\n    \n    @ViewBuilder\n    func linkButton(_ title: String, systemImage: String, destination: URL) -> some View {\n        Link(destination: destination) {\n            Label(title, systemImage: systemImage)\n                .frame(maxWidth: .infinity)\n                .padding(.vertical, 10)\n                .background(\n                    RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius)\n                        .fill(Color(uiColor: .secondarySystemFill))\n                )\n        }\n        .buttonStyle(.plain)\n        .padding(.horizontal, Constants.main.standardSpacing)\n    }\n}\n\n// swiftlint:enable line_length\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/FediseerOpinionListView.swift",
    "content": "//\n//  FediseerOpinionListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 04/02/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct FediseerOpinionListView: View {\n    let instance: Instance\n    let opinionType: FediseerOpinionType\n    let fediseerData: FediseerData\n    \n    var body: some View {\n        FancyScrollView {\n            VStack(spacing: 16) {\n                let items = fediseerData.opinions(ofType: opinionType).sorted {\n                    $0.reason != nil && $1.reason == nil\n                }\n                \n                ForEach(items, id: \\.domain) { opinion in\n                    FediseerOpinionView(opinion: opinion)\n                }\n            }\n            .padding(16)\n        }\n        .themedGroupedBackground()\n        .navigationTitle(opinionType.label)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/FediseerOpinionView.swift",
    "content": "//\n//  EndorsementView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 03/02/2024.\n//\n\nimport ComponentViews\nimport LemmyMarkdownUI\nimport SwiftUI\n\nstruct FediseerOpinionView: View {\n    @Environment(\\.palette) var palette\n    \n    let opinion: any FediseerOpinion\n    \n    var body: some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            HStack {\n                if let stub = opinion.instanceStub {\n                    NavigationLink(.instanceStub(stub)) { title }\n                        .buttonStyle(.plain)\n                } else {\n                    title\n                }\n                Spacer()\n            }\n            .foregroundStyle(type(of: opinion).color)\n            .padding(.horizontal)\n            divider\n            if let reason = opinion.formattedReason {\n                Markdown(reason, configuration: .default(palette: palette))\n                    .padding(.horizontal)\n            } else {\n                Text(\"No reason given\")\n                    .foregroundStyle(.themedSecondary)\n                    .italic()\n                    .padding(.leading)\n            }\n            if let evidence = opinion.evidence {\n                divider\n                Markdown(evidence, configuration: .default(palette: palette))\n                    .padding(.horizontal)\n            }\n        }\n        .frame(maxWidth: .infinity)\n        .padding(.vertical, 10)\n        .font(.callout)\n        .background(.themedSecondaryGroupedBackground)\n        .cornerRadius(Constants.main.standardSpacing)\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    var title: some View {\n        Image(icon: type(of: opinion).icon)\n            .fontWeight(.semibold)\n            .symbolVariant(.fill)\n            .foregroundStyle(.secondary) // Don't use palette here\n        Text(opinion.domain)\n            .fontWeight(.semibold)\n    }\n    \n    @ViewBuilder\n    var divider: some View {\n        Line()\n            .stroke(style: StrokeStyle(lineWidth: 2, dash: [5]))\n            .frame(height: 2)\n            .foregroundStyle(.themedGroupedBackground)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/InstanceCommunityListView.swift",
    "content": "//\n//  InstanceCommunityListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-11-09.\n//  \n\nimport MlemMiddleware\nimport SwiftUI\nimport os\n\nstruct InstanceCommunityListView: View {\n    let logger: Logger = .mlemLogger()\n    \n    let communityLoader: CommunityFeedLoader\n\n    @Binding var errorDetails: ErrorDetails?\n\n    var body: some View {\n        LazyVStack(spacing: 0) {\n            if let errorDetails {\n                ErrorView(errorDetails)\n                    .padding(.top, 30)\n            } else {\n                SearchResultsView(results: communityLoader.items) { community in\n                    CommunityListRow(\n                        community,\n                        readout: .subscribers,\n                        visitContext: .other\n                    )\n                    .onAppear {\n                        do {\n                            try communityLoader.loadIfThreshold(community)\n                        } catch {\n                            handleError(error)\n                        }\n                    }\n                }\n                EndOfFeedView(feedLoader: communityLoader, viewType: .hobbit)\n            }\n        }\n        .animation(.easeOut(duration: 0.1), value: communityLoader.items.isEmpty)\n        .task {\n            logger.error(\"\\(String(describing: errorDetails?.errorText()))\")\n            if errorDetails == nil {\n                await refresh()\n            }\n        }\n    }\n\n    func refresh() async {\n        do {\n            if communityLoader.loadingState == .initial {\n                try await communityLoader.refresh(listing: .local)\n            }\n            self.errorDetails = nil\n        } catch {\n            var errorDetails = handleErrorWithDetails(error)\n\n            errorDetails?.refresh = {\n                await refresh()\n                return true\n            }\n\n            if case let ApiClientError.response(response, _) = error {\n                if response.instanceIsPrivate {\n                    errorDetails?.title = String(localized: \"Instance is private\")\n                    errorDetails?.body = String(localized: \"You cannot view the communities of a private instance.\")\n                    errorDetails?.icon = .lemmy.private\n                    errorDetails?.refresh = nil\n                }\n            }\n\n            self.errorDetails = errorDetails\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/InstanceDetailsView.swift",
    "content": "//\n//  InstanceStatsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 20/01/2024.\n//\n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\n// swiftlint:disable:next type_body_length\nstruct InstanceDetailsView: View {\n    @State private var showingSlurRegex: Bool = false\n    @State var uptimeData: UptimeDataStatus?\n    \n    let instance: Instance\n    \n    var body: some View {\n        content\n            .task {\n                let fetchedData = await loadUptimeData(instance: instance)\n                withAnimation(.easeOut(duration: 0.2)) {\n                    uptimeData = fetchedData\n                }\n            }\n    }\n    \n    var content: some View {\n        VStack(spacing: 16) {\n            if instance.api.supports(.viewInstanceCreationDate, defaultValue: true) {\n                FormSection {\n                    ProfileDateView(profilable: instance)\n                        .padding(.vertical, Constants.main.standardSpacing)\n                }\n            }\n            \n            statsView\n            \n            FormSection {\n                if case let .success(uptimeData) = uptimeData {\n                    NavigationLink(.instanceUptime(instance, uptimeData: uptimeData)) {\n                        uptimeSummary\n                    }\n                    .buttonStyle(.plain)\n                } else {\n                    uptimeSummary\n                }\n            }\n            \n            if instance.api.supports(.viewInstanceSettings, defaultValue: true) {\n                settingsListView\n            }\n        }\n        .padding([.horizontal, .bottom], 16)\n    }\n    \n    @ViewBuilder\n    var statsView: some View {\n        HStack(spacing: 16) {\n            ExpectedView(instance.userCount) { userCount in\n                FormReadout(\"Users\", value: userCount)\n                    .tint(.themedPersonAccent)\n            }\n            ExpectedView(instance.communityCount) { communityCount in\n                FormReadout(\"Communities\", value: communityCount)\n                    .tint(.themedCommunityAccent)\n            }\n        }\n        .frame(maxWidth: .infinity)\n        \n        HStack(spacing: 16) {\n            ExpectedView(instance.postCount) { postCount in\n                FormReadout(\"Posts\", value: postCount)\n                    .tint(.themedPostAccent)\n            }\n            ExpectedView(instance.commentCount) { commentCount in\n                FormReadout(\"Comments\", value: commentCount)\n                    .tint(.themedCommentAccent)\n            }\n        }\n        .frame(maxWidth: .infinity)\n        \n        ExpectedView(instance.activeUserCount) { activeUserCount in\n            ActiveUserCountView(activeUserCount: activeUserCount)\n        }\n    }\n    \n    @ViewBuilder\n    var settingsListView: some View {\n        FormSection {\n            VStack(alignment: .leading, spacing: 0) {\n                ExpectedView(instance.isPrivate) { isPrivate in\n                    settingRow(\n                        \"Private\",\n                        icon: .lemmy.private,\n                        value: isPrivate\n                    )\n                }\n                Divider()\n                ExpectedView(instance.federationEnabled) { federationEnabled in\n                    settingRow(\n                        \"Federates\",\n                        icon: .lemmy.federation,\n                        value: federationEnabled\n                    )\n                }\n            }\n        }\n        \n        FormSection {\n            ExpectedView(instance.registrationMode) { registrationMode in\n                VStack(alignment: .leading, spacing: 0) {\n                    settingRow(\n                        \"Registration\",\n                        icon: .lemmy.person,\n                        value: registrationMode.label,\n                        color: registrationMode.color\n                    )\n                    if registrationMode != .closed {\n                        Divider()\n                        ExpectedView(instance.emailVerificationRequired) { emailVerificationRequired in\n                            settingRow(\n                                \"Email Verification\",\n                                icon: .general.email,\n                                value: emailVerificationRequired\n                            )\n                        }\n                        Divider()\n                        ExpectedView(instance.captchaDifficulty) { captchaDifficulty in\n                            settingRow(\n                                \"Captcha\",\n                                icon: .lemmy.captcha,\n                                value: captchaLabel(for: captchaDifficulty),\n                                color: captchaDifficulty == nil ? .themedNegative : .themedPositive\n                            )\n                        }\n                    }\n                }\n            }\n        }\n\n        FormSection {\n            ExpectedView(instance.voteFederationMode) { voteMode in\n                VStack(alignment: .leading, spacing: 0) {\n                    voteFederationRow(\n                        \"Post Upvotes\",\n                        type: .upvote,\n                        value: voteMode.postUpvote\n                    )\n                    Divider()\n                    voteFederationRow(\n                        \"Post Downvotes\",\n                        type: .downvote,\n                        value: voteMode.postDownvote\n                    )\n                    Divider()\n                    voteFederationRow(\n                        \"Comment Upvotes\",\n                        type: .upvote,\n                        value: voteMode.commentUpvote\n                    )\n                    Divider()\n                    voteFederationRow(\n                        \"Comment Downvotes\",\n                        type: .downvote,\n                        value: voteMode.commentDownvote\n                    )\n                }\n            }\n        }\n        \n        FormSection {\n            VStack(alignment: .leading, spacing: 0) {\n                ExpectedView(instance.nsfwContentEnabled) { nsfwContentEnabled in\n                    settingRow(\n                        \"NSFW Content\",\n                        icon: .settings.blurNsfw,\n                        value: nsfwContentEnabled\n                    )\n                }\n                Divider()\n                ExpectedView(instance.communityCreationRestrictedToAdmins) { communityCreationRestrictedToAdmins in\n                    settingRow(\n                        \"Community Creation\",\n                        icon: .lemmy.community,\n                        value: !communityCreationRestrictedToAdmins\n                    )\n                }\n                Divider()\n                // ExpectedView causes rendering issues here\n                if let slurFilterRegex = instance.slurFilterRegex.value {\n                    Group {\n                        settingRow(\n                            \"Slur Filter\",\n                            icon: .general.filter,\n                            value: slurFilterRegex != nil\n                        )\n                        if let slurFilterRegex {\n                            Divider()\n                            VStack(alignment: .leading, spacing: 2) {\n                                if showingSlurRegex {\n                                    Text(slurFilterRegex)\n                                        .foregroundStyle(.themedSecondary)\n                                        .textSelection(.enabled)\n                                } else {\n                                    Text(\"Tap to show slur filter regex.\")\n                                    Label(\n                                        \"This probably contains foul language.\",\n                                        icon: .general.warning\n                                    )\n                                    .foregroundStyle(.themedCaution)\n                                }\n                            }\n                            .font(.footnote)\n                            .frame(maxWidth: .infinity, alignment: .leading)\n                            .padding(12)\n                            .contentShape(.rect)\n                            .onTapGesture {\n                                withAnimation(.easeInOut(duration: 0.2)) {\n                                    showingSlurRegex.toggle()\n                                }\n                            }\n                        }\n                    }\n                }\n                Divider()\n                ExpectedView(instance.defaultFeed) { defaultFeed in\n                    settingRow(\n                        \"Default Feed Type (Desktop)\",\n                        icon: .lemmy.feed,\n                        value: defaultFeed.label\n                    )\n                }\n            }\n        }\n        \n        FormSection {\n            VStack(alignment: .leading, spacing: 0) {\n                ExpectedView(instance.hideModlogNames) { hideModlogNames in\n                    settingRow(\n                        \"Show Mod Names in Modlog\",\n                        icon: .lemmy.moderation,\n                        value: !hideModlogNames\n                    )\n                }\n                Divider()\n                ExpectedView(instance.emailApplicationsToAdmins) { emailApplicationsToAdmins in\n                    settingRow(\n                        \"Applications Email Admins\",\n                        icon: .lemmy.registrationApplication,\n                        value: emailApplicationsToAdmins\n                    )\n                }\n                Divider()\n                ExpectedView(instance.emailReportsToAdmins) { emailReportsToAdmins in\n                    settingRow(\n                        \"Reports Email Admins\",\n                        icon: .lemmy.report,\n                        value: emailReportsToAdmins\n                    )\n                }\n            }\n        }\n    }\n\n    @ViewBuilder\n    func settingRow(\n        _ label: LocalizedStringResource,\n        icon: Icon,\n        value: LocalizedStringResource,\n        color: ThemedColor? = nil\n    ) -> some View {\n        HStack {\n            Image(icon: icon)\n                .foregroundStyle(.themedSecondary)\n                .frame(width: 30)\n            Text(label)\n            Spacer()\n            Text(value)\n                .foregroundStyle(color ?? .themedPrimary)\n        }\n        .padding(12)\n    }\n    \n    func captchaLabel(for diff: CaptchaDifficulty?) -> LocalizedStringResource {\n        if let diff {\n            return .init(\n                \"Captcha Difficulty Yes\",\n                defaultValue: \"Yes (\\(diff.label))\",\n                comment: \"Used to indicate Captcha difficulty. E.g. \\\"Yes (Hard)\\\".\"\n            )\n        }\n        return \"No\"\n    }\n\n    @ViewBuilder\n    func settingRow(_ label: LocalizedStringResource, icon: Icon, value: Bool) -> some View {\n        settingRow(\n            label,\n            icon: icon,\n            value: value ? \"Yes\" : \"No\",\n            color: value ? .themedPositive : .themedNegative\n        )\n    }\n\n    @ViewBuilder\n    func voteFederationRow(\n        _ label: LocalizedStringResource,\n        type: ScoringOperation,\n        value: FederationMode\n    ) -> some View {\n        settingRow(\n            label,\n            icon: type.icon,\n            value: value.label,\n            color: value.color\n        )\n    }\n    \n    @ViewBuilder\n    var uptimeSummary: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            HStack {\n                Text(\"Uptime\")\n                \n                Spacer()\n                \n                if case .success = uptimeData {\n                    (Text(\"Details\") + Text(verbatim: \" \") + Text(Image(icon: .general.forward)))\n                        .font(.footnote)\n                        .foregroundStyle(.themedAccent)\n                }\n            }\n            \n            switch uptimeData {\n            case let .success(uptimeData):\n                RecentUptimeChecks(results: uptimeData.results)\n            case .unavailable:\n                Text(\"Data not available\")\n                    .italic()\n                    .foregroundStyle(.themedWarning)\n                    .frame(maxWidth: .infinity, alignment: .leading)\n            case let .failure(error):\n                ErrorView(.init(error: error))\n            default:\n                ProgressView()\n                    .padding(Constants.main.halfSpacing)\n            }\n        }\n        .padding(Constants.main.standardSpacing)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/InstanceSafetyView.swift",
    "content": "//\n//  InstanceSafetyView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 03/02/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct InstanceSafetyView: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.palette) var palette\n    \n    let instance: Instance\n    let fediseerData: FediseerData\n        \n    var body: some View {\n        VStack(alignment: .leading, spacing: 0) {\n            section { guarantorView }\n            HStack {\n                Button(\"Learn more...\") {\n                    navigation.openSheet(.fediseerInfo)\n                }\n                .buttonStyle(.plain)\n                Spacer()\n                if let url = URL(string: \"https://gui.fediseer.com/instances/detail/\\(instance.name)\") {\n                    Link(destination: url) {\n                        Text(\"Fediseer GUI\")\n                        Image(systemName: \"arrow.up.forward\")\n                    }\n                }\n            }\n            .font(.footnote)\n            .foregroundStyle(.tint)\n            .padding(.horizontal, 6)\n            .padding(.top, 7)\n            .padding(.bottom, 30)\n            \n            opinionsView\n        }\n        .frame(maxWidth: .infinity)\n        .padding(.horizontal, 16)\n    }\n    \n    @ViewBuilder\n    var guarantorView: some View {\n        VStack(alignment: .leading, spacing: 6) {\n            HStack {\n                if fediseerData.instance.guarantor != nil {\n                    Label(\"Guaranteed\", icon: .fediseer.guarantee)\n                        .foregroundStyle(.themedPositive)\n                } else if fediseerData.censures?.isEmpty ?? true {\n                    Label(\"Not Guaranteed\", icon: .fediseer.unguarantee)\n                        .foregroundStyle(.themedSecondary)\n                } else {\n                    Label(\"Censured\", icon: .fediseer.censure)\n                        .foregroundStyle(.themedNegative)\n                }\n                Spacer()\n            }\n            .symbolVariant(.fill)\n            .fontWeight(.semibold)\n            .font(.title2)\n            Text(summaryCaption)\n                .foregroundStyle(.themedSecondary)\n                .font(.footnote)\n        }\n        .frame(maxWidth: .infinity)\n        .padding(.vertical, Constants.main.standardSpacing)\n        .padding(.horizontal)\n    }\n    \n    var summaryCaption: String {\n        if let guarantor = fediseerData.instance.guarantor {\n            .init(localized: \"\\(instance.name) is guaranteed by \\(guarantor).\")\n        } else if fediseerData.censures?.isEmpty ?? true {\n            .init(localized: \"This instance is not part of the Fediseer Chain of Trust.\")\n        } else {\n            .init(localized: \"This instance is viewed very negatively by one or more trusted instances.\")\n        }\n    }\n    \n    @ViewBuilder\n    var opinionsView: some View {\n        VStack(spacing: 22) {\n            let opinionTypes = FediseerOpinionType.allCases.sorted {\n                fediseerData.opinions(ofType: $0).count > fediseerData.opinions(ofType: $1).count\n            }\n            ForEach(opinionTypes, id: \\.self) { opinionType in\n                let items = fediseerData.opinions(ofType: opinionType).sorted {\n                    $0.reason != nil && $1.reason == nil\n                }\n                \n                if !items.isEmpty {\n                    VStack(alignment: .leading, spacing: 7) {\n                        let destination: NavigationPage = .instanceOpinionList(\n                            instance: instance,\n                            opinionType: opinionType,\n                            data: fediseerData\n                        )\n                        opinionSubheading(\n                            title: opinionType.label,\n                            count: items.count,\n                            destination: items.count > 5 ? destination : nil\n                        )\n                        ForEach(items.prefix(5), id: \\.domain) { item in\n                            FediseerOpinionView(opinion: item)\n                                .padding(.bottom, 9)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    @ViewBuilder func section(spacing: CGFloat = 5, @ViewBuilder content: () -> some View) -> some View {\n        VStack(alignment: .leading, spacing: 7) {\n            VStack(alignment: .leading, spacing: spacing) {\n                content()\n            }\n            .frame(maxWidth: .infinity)\n            .background(.themedSecondaryGroupedBackground)\n            .cornerRadius(Constants.main.standardSpacing)\n            .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        }\n    }\n    \n    @ViewBuilder\n    func opinionSubheading(title: String, count: Int, destination: NavigationPage? = nil) -> some View {\n        VStack(alignment: .leading, spacing: 0) {\n            HStack(spacing: 0) {\n                (Text(title) + Text(verbatim: \" (\\(count))\").foregroundColor(palette.label.secondary))\n                    .font(.title2)\n                    .fontWeight(.semibold)\n                Spacer()\n                if let destination {\n                    NavigationLink(\"See All\", destination: destination)\n                        .foregroundStyle(.tint)\n                        .buttonStyle(.plain)\n                }\n            }\n        }\n        .padding(.horizontal, 6)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/InstanceStubResolutionPage.swift",
    "content": "//\n//  InstanceStubResolutionPage.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-03-22.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct InstanceStubResolutionPage: View {\n    @Environment(NavigationLayer.self) var navigation\n    \n    let stub: InstanceStub\n    let targetPage: (Instance) -> NavigationPage\n    \n    @State var upgradeError: Error?\n    \n    var body: some View {\n        content\n            .themedGroupedBackground()\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        if let upgradeError {\n            ErrorView(.init(\n                error: upgradeError,\n                refresh: fetchInstance\n            ))\n        } else {\n            ProgressView()\n                .task {\n                    await fetchInstance()\n                }\n        }\n    }\n    \n    @discardableResult\n    func fetchInstance() async -> Bool {\n        do {\n            let instance = try await stub.getLocalInstance()\n            navigation.replace(targetPage(instance))\n            return true\n        } catch {\n            upgradeError = error\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/InstanceUptimeView+Logic.swift",
    "content": "//\n//  InstanceUptimeView+Logic..swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-05-08.\n//\n\nimport Foundation\nimport MlemMiddleware\n\nenum UptimeDataStatus {\n    case success(UptimeData)\n    case unavailable\n    case failure(Error)\n}\n\nfunc loadUptimeData(instance: Instance) async -> UptimeDataStatus {\n    if let url = instance.uptimeDataUrl {\n        do {\n            let data = try await URLSession.shared.data(from: url).0\n            let uptimeData = try JSONDecoder.defaultDecoder.decode(UptimeData.self, from: data)\n            return .success(uptimeData)\n        } catch {\n            handleError(error)\n            return .failure(error)\n        }\n    } else {\n        return .unavailable\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/InstanceUptimeView+Views.swift",
    "content": "//\n//  InstanceUptimeView+Views.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-05-09.\n//\n\nimport SwiftUI\n\nstruct RecentUptimeChecks: View {\n    @Environment(\\.accessibilityDifferentiateWithoutColor) var diffWithoutColor: Bool\n    \n    let results: [UptimeResponseTime]\n    \n    var body: some View {\n        VStack(alignment: .leading, spacing: 3) {\n            HStack(spacing: 3) {\n                ForEach(results) { result in\n                    Group {\n                        if diffWithoutColor {\n                            Image(icon: result.success ? .uptime.online : .uptime.offline)\n                                .resizable()\n                                .aspectRatio(contentMode: .fit)\n                                .symbolVariant(.circle.fill)\n                        } else {\n                            Circle()\n                        }\n                    }\n                    .foregroundStyle(result.success ? .themedPositive : .themedNegative)\n                    .frame(maxWidth: 20)\n                    .frame(maxWidth: 25)\n                }\n            }\n            HStack {\n                Text(timeOnlyFormatter.string(from: results.first?.timestamp ?? .now))\n                Spacer()\n                Text(timeOnlyFormatter.string(from: results.last?.timestamp ?? .now))\n            }\n            .font(.footnote)\n            .foregroundStyle(.themedSecondary)\n            .frame(maxWidth: CGFloat(results.count * 25 + (results.count - 1) * 3))\n            .padding(.top, 4)\n        }\n    }\n    \n    var timeOnlyFormatter: DateFormatter {\n        let dateFormatter = DateFormatter()\n        dateFormatter.timeStyle = .short\n        dateFormatter.dateStyle = .none\n        return dateFormatter\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/InstanceUptimeView.swift",
    "content": "//\n//  InstanceUptimeView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 28/01/2024.\n//\n\nimport Charts\nimport MlemMiddleware\nimport SwiftUI\n\nstruct InstanceUptimeView: View {\n    @Environment(\\.accessibilityDifferentiateWithoutColor) var diffWithoutColor: Bool\n    @Environment(\\.palette) var palette\n    \n    @State var showingExactTime: Bool = false\n    @State var showingAllDowntimes: Bool = false\n    \n    let instance: Instance\n    @State var uptimeData: UptimeData\n    \n    var uptimeRefreshTimer = Timer.publish(every: 30, tolerance: 0.5, on: .main, in: .common)\n        .autoconnect()\n    \n    var body: some View {\n        ScrollView {\n            content\n                .padding(.top, 16)\n        }\n        .background(.themedGroupedBackground)\n        .onReceive(uptimeRefreshTimer) { _ in\n            Task {\n                let uptimeStatus = await loadUptimeData(instance: instance)\n                switch uptimeStatus {\n                case let .success(uptimeData):\n                    withAnimation(.easeOut(duration: 0.2)) {\n                        self.uptimeData = uptimeData\n                    }\n                case .unavailable:\n                    assertionFailure(\"Uptime data unavailable.\")\n                case let .failure(error):\n                    handleError(error)\n                }\n            }\n        }\n        .navigationTitle(\"Uptime\")\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        VStack(alignment: .leading, spacing: 0) {\n            section { summary }\n                .padding(.bottom, 10)\n            section(\"Recent Checks\") {\n                RecentUptimeChecks(results: uptimeData.results)\n                    .padding(.horizontal)\n                    .padding(.vertical, 15)\n            }\n            .padding(.top, 20)\n            // String interpolation used here to avoid localizing the number\n            footnote(\"Automatically refreshes every \\(30) seconds.\")\n                .padding(.top, 8)\n                .padding(.leading, 6)\n                .padding(.bottom, 30)\n            section(\"Response Time\") {\n                VStack(alignment: .leading, spacing: 4) {\n                    responseTimeChart\n                        .padding(.horizontal, 20)\n                    let milliseconds = uptimeData.results.map(\\.durationMs).reduce(0, +) / uptimeData.results.count\n                    footnote(\"Average: \\(formatMilliseconds(milliseconds))\")\n                        .padding(.leading, 20)\n                }\n                .padding(.top, 17)\n                .padding(.bottom, 8)\n            }\n            subHeading(\"Incidents\")\n                .padding(.top, 30)\n                .padding(.bottom, 3)\n            let todayDowntimes = uptimeData.downtimes.filter { abs($0.endTime.timeIntervalSinceNow) < 60 * 60 * 24 }\n            \n            Text(\n                todayDowntimes.count == 0\n                    ? \"There were no recorded incidents today.\"\n                    : \"There were \\(todayDowntimes.count) recorded incidents today.\"\n            )\n            .font(.footnote)\n            .foregroundStyle(.themedSecondary)\n            .padding(.leading, 6)\n            .padding(.bottom, 7)\n            \n            let displayedIncidents = showingAllDowntimes ? uptimeData.downtimes : todayDowntimes\n            if !displayedIncidents.isEmpty {\n                section(spacing: 0) {\n                    ForEach(displayedIncidents) { event in\n                        if event.id != uptimeData.downtimes.first?.id {\n                            Divider()\n                        }\n                        IncidentRow(event: event, showingExactTime: showingExactTime)\n                            .padding(.vertical, 10)\n                            .padding(.leading)\n                    }\n                    .onTapGesture {\n                        withAnimation(.easeInOut(duration: 0.2)) {\n                            showingExactTime.toggle()\n                        }\n                    }\n                }\n                .padding(.bottom, 30)\n            }\n            Button {\n                withAnimation {\n                    showingAllDowntimes.toggle()\n                }\n            } label: {\n                Text(showingAllDowntimes ? \"Hide Older Incidents\" : \"Show Older Incidents\")\n                    .foregroundStyle(.themedAccent)\n                    .padding(.leading, 12)\n                    .frame(maxWidth: .infinity, alignment: .leading)\n                    .padding(.vertical, 10)\n                    .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n            }\n            .buttonStyle(.empty)\n            \n            if let url = instance.uptimeFrontendUrl {\n                // Extra string interpolation used here to avoid unnecessary localization\n                Text(\n                    (try? AttributedString(\n                        markdown: .init(localized: \"Uptime data fetched from \\(\"[lemmy-status.org](\\(url))\")\"))\n                    ) ?? .init()\n                )\n                .font(.footnote)\n                .foregroundStyle(.themedSecondary)\n                .padding(.vertical, 8)\n                .padding(.leading, 6)\n            }\n        }\n        .padding([.horizontal, .bottom], 16)\n    }\n    \n    @ViewBuilder\n    var summary: some View {\n        VStack(alignment: .leading) {\n            if let mostRecentOutage = uptimeData.downtimes.first {\n                if uptimeData.results.filter(\\.success).count < 15 {\n                    summaryHeader(isHealthy: false)\n                    footnote(\"\\(instance.name) has been unresponsive recently.\")\n                } else {\n                    summaryHeader(isHealthy: true)\n                    let relTime = mostRecentOutage.relativeTimeCaption\n                    let length = mostRecentOutage.differenceTitle(unitsStyle: .full)\n                    footnote(\"The most recent outage was \\(relTime), and lasted for \\(length).\")\n                }\n            }\n        }\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .padding(.vertical, 10)\n        .padding(.horizontal)\n    }\n    \n    @ViewBuilder\n    func summaryHeader(isHealthy: Bool) -> some View {\n        HStack(spacing: 5) {\n            summaryHeaderText(isHealthy: isHealthy)\n                .font(.title2)\n            Image(icon: isHealthy ? .uptime.online : .uptime.outage)\n                .symbolVariant(.circle.fill)\n                .foregroundStyle(isHealthy ? .themedPositive : .themedNegative)\n        }\n        .fontWeight(.semibold)\n    }\n    \n    func summaryHeaderText(isHealthy: Bool) -> some View {\n        let resource: LocalizedStringResource\n        let color: Color\n        if isHealthy {\n            resource = .init(\n                \"\\(instance.name) is {{online}}\",\n                comment: \"The word(s) within the curly brackets will be colored green.\"\n            )\n            color = palette.positive\n        } else {\n            resource = .init(\n                \"\\(instance.name) is {{unhealthy}}\",\n                comment: \"The word(s) within the curly brackets will be colored red.\"\n            )\n            color = palette.negative\n        }\n        let string = String(localized: resource)\n        let parts = string.split(separator: /\\{\\{|\\}\\}/, omittingEmptySubsequences: false)\n        guard parts.count == 3 else {\n            assertionFailure()\n            return Text(string)\n        }\n        return Text(parts[0]) + Text(parts[1]).foregroundColor(color) + Text(parts[2])\n    }\n    \n    @ViewBuilder\n    var responseTimeChart: some View {\n        Chart {\n            ForEach(uptimeData.results) { node in\n                let time = Int(node.durationMs)\n                LineMark(\n                    x: .value(\"Time\", node.timestamp),\n                    y: .value(\"Response Time\", time)\n                )\n            }\n        }\n        .frame(height: 200)\n        .chartXAxis {\n            let marks = [uptimeData.results.first?.timestamp ?? .distantPast, uptimeData.results.last?.timestamp ?? .distantFuture]\n            AxisMarks(format: .dateTime.hour(.defaultDigits(amPM: .abbreviated)).minute(.twoDigits), values: marks)\n        }\n        .chartYScale(domain: [0, max(1000, (uptimeData.results.map(\\.durationMs).max() ?? 0) + 100)])\n        .chartYAxis {\n            AxisMarks(values: .automatic) { value in\n                AxisGridLine()\n                AxisValueLabel {\n                    if let intValue = value.as(Int.self) {\n                        Text(formatMilliseconds(intValue))\n                    }\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder func section(\n        _ title: LocalizedStringResource? = nil,\n        spacing: CGFloat = 5,\n        @ViewBuilder content: () -> some View\n    ) -> some View {\n        VStack(alignment: .leading, spacing: 7) {\n            if let title {\n                subHeading(title)\n            }\n            VStack(alignment: .leading, spacing: spacing) {\n                content()\n            }\n            .frame(maxWidth: .infinity)\n            .background(.themedSecondaryGroupedBackground)\n            .cornerRadius(Constants.main.standardSpacing)\n            .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        }\n    }\n    \n    @ViewBuilder\n    func subHeading(_ title: LocalizedStringResource) -> some View {\n        Text(title)\n            .font(.title2)\n            .fontWeight(.semibold)\n            .padding(.leading, 6)\n    }\n    \n    @ViewBuilder\n    func footnote(_ title: LocalizedStringResource) -> some View {\n        Text(title)\n            .font(.footnote)\n            .foregroundStyle(.themedSecondary)\n    }\n    \n    @_disfavoredOverload\n    @ViewBuilder\n    func footnote(_ title: String) -> some View {\n        Text(title)\n            .font(.footnote)\n            .foregroundStyle(.themedSecondary)\n    }\n\n    func formatMilliseconds(_ milliseconds: Int) -> String {\n        let measurement = Measurement(value: Double(milliseconds), unit: UnitDuration.milliseconds)\n        let formatter = MeasurementFormatter()\n        formatter.unitOptions = .providedUnit\n        formatter.unitStyle = .short\n        return formatter.string(from: measurement)\n    }\n}\n\nprivate struct IncidentRow: View {\n    let event: DowntimePeriod\n    let showingExactTime: Bool\n    \n    var body: some View {\n        VStack(alignment: .leading) {\n            HStack {\n                Image(icon: .uptime.outage)\n                    .symbolVariant(.fill)\n                    .foregroundStyle(event.severityColor)\n                    .foregroundStyle(.themedSecondary)\n                Text(\"Unhealthy for \\(event.differenceTitle())\")\n            }\n            Text(showingExactTime ? event.differenceCaption : event.relativeTimeCaption)\n                .font(.footnote)\n                .foregroundStyle(.themedSecondary)\n        }\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .contentShape(.rect)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/InstanceView+About.swift",
    "content": "//\n//  InstanceView+About.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-11.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nextension InstanceView {\n    @ViewBuilder\n    var aboutTab: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            if let shortDescription = instance.shortDescription {\n                markdownBox(shortDescription)\n            }\n            if let description = instance.description {\n                markdownBox(description)\n            }\n        }\n        .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n    }\n    \n    private func markdownBox(_ text: String) -> some View {\n        Markdown(text, configuration: .default(palette: palette))\n            .padding(Constants.main.standardSpacing)\n            .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n            .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/InstanceView+Logic.swift",
    "content": "//\n//  InstanceView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 23/09/2024.\n//\n\nimport MlemMiddleware\nimport QuickSwipes\nimport SwiftUI\n\nextension InstanceView {\n    var availableTabs: [Tab] {\n        var result: [Tab] = []\n        result.append(.about)\n        if instance.api.supports(.searchLocalCommunities, defaultValue: true) {\n            result.append(.communities)\n        }\n        result += [.administration, .details, .safety]\n        return result\n    }\n    \n    func logVisit(_ instance: Instance) {\n        guard let visitContext else { return }\n        if let session = (appState.firstSession as? UserSession),\n           let visitHistory = session.visitHistory,\n           let summary = instance.instanceSummary {\n            visitHistory.addInstance(summary, context: visitContext)\n            Task(priority: .background) {\n                try await session.saveVisitHistory()\n            }\n        }\n    }\n    \n    func openAddAdminSheet() {\n        navigation.openSheet(.personPicker(filter: .local) { person in\n            newAdmin = person\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {\n                showingConfirmation = true\n            }\n        })\n    }\n    \n    func addNewAdmin() {\n        guard let newAdmin else {\n            assertionFailure(\"newAdmin cannot be nil\")\n            return\n        }\n        guard newAdmin.apiIsLocal else {\n            ToastModel.main.add(.error(.init(title: \"Cannot appoint non-local user as administrator\")))\n            return\n        }\n        guard instance.local || instance.host == \"localhost\" else {\n            assertionFailure(\"Instance is not local\")\n            return\n        }\n        \n        instance.addAdmin(personId: newAdmin.id, added: true)\n    }\n    \n    func administratorQuickSwipes(person: Person) -> SwipeConfiguration {\n        guard let myPerson = appState.firstPerson,\n              myPerson.api.isHigherAdmin(than: person),\n              let myInstance = appState.firstApi.myInstance,\n              let isAdmin = person.isAdmin.value else {\n            return .init()\n        }\n        \n        return .init(trailingActions: [person.addAdminAction(instance: myInstance, isOn: isAdmin)])\n    }\n    \n    func attemptToLoadFediseerData() {\n        if fediseerData == nil {\n            let host = instance.host\n            Task {\n                do {\n                    guard let instanceURL = URL(string: \"https://fediseer.com/api/v1/whitelist/\\(host)\") else { return }\n                    async let instanceData = try await URLSession.shared.data(from: instanceURL).0\n                    \n                    async let endorsementsData = try await URLSession.shared.data(\n                        from: URL(string: \"https://fediseer.com/api/v1/endorsements/\\(host)\")!\n                    ).0\n                    \n                    async let hesitationsData = try await URLSession.shared.data(\n                        from: URL(string: \"https://fediseer.com/api/v1/hesitations/\\(host)\")!\n                    ).0\n                    \n                    async let censuresData = try await URLSession.shared.data(\n                        from: URL(string: \"https://fediseer.com/api/v1/censures/\\(host)\")!\n                    ).0\n                    \n                    let decoder = JSONDecoder()\n                    decoder.keyDecodingStrategy = .convertFromSnakeCase\n                    \n                    let fediseerData = try await FediseerData(\n                        instance: decoder.decode(\n                            FediseerInstance.self,\n                            from: instanceData\n                        ),\n                        endorsements: decoder.decode(\n                            FediseerEndorsements.self,\n                            from: endorsementsData\n                        ).instances,\n                        hesitations: decoder.decode(\n                            FediseerHesitations.self,\n                            from: hesitationsData\n                        ).instances,\n                        censures: decoder.decode(\n                            FediseerCensures.self,\n                            from: censuresData\n                        ).instances\n                    )\n                    \n                    Task { @MainActor in\n                        withAnimation(.easeOut(duration: 0.2)) {\n                            self.fediseerData = fediseerData\n                        }\n                    }\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n    }\n\n    func refresh() async {\n        guard upgradeState == .idle else { return }\n        upgradeState = .loading\n        do {\n            if !instance.apiIsLocal {\n                instance = try await instance.getLocal()\n            }\n            upgradeState = .done\n            errorDetails = nil\n        } catch {\n            upgradeState = .idle\n            var errorDetails = handleErrorWithDetails(error)\n\n            errorDetails?.refresh = {\n                await refresh()\n                return true\n            }\n\n            if case let ApiClientError.decoding(data, _) = error {\n                let string = String(data: data, encoding: .utf8)\n                if string?.contains(\"<title>Just a moment...</title>\") ?? false {\n                    errorDetails?.title = .init(localized: \"Blocked by Cloudflare\")\n                    errorDetails?.icon = .general.cloudflare\n                    errorDetails?.body = .init(localized: \"This page can't be displayed because Cloudflare blocked the request.\")\n                    errorDetails?.refresh = nil\n                }\n            }\n\n            self.errorDetails = errorDetails\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/InstanceView.swift",
    "content": "//\n//  InstanceView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/07/2024.\n//\n\nimport ComponentViews\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct InstanceView: View {\n    enum Tab: String, CaseIterable, Identifiable {\n        case about, communities, administration, details, safety\n        \n        var label: LocalizedStringResource {\n            switch self {\n            case .about: \"About\"\n            case .communities: \"Communities\"\n            case .administration: \"Administration\"\n            case .details: \"Details\"\n            case .safety: \"Trust & Safety\"\n            }\n        }\n        \n        var id: Self { self }\n    }\n    \n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.palette) var palette\n    @Environment(\\.colorScheme) var colorScheme\n    \n    let visitContext: VisitHistory.VisitContext?\n\n    // This is fetched from the instance itself, not from the logged-in account.\n    @State var instance: Instance\n    @State var fediseerData: FediseerData?\n    @State var upgradeState: LoadingState = .idle\n    @State var communityLoader: CommunityFeedLoader\n    \n    @State var selectedTab: Tab = .about\n    \n    @State var showingConfirmation: Bool = false\n    @State var newAdmin: Person?\n\n    @State var errorDetails: ErrorDetails?\n    @State var communityListErrorDetails: ErrorDetails?\n    \n    init(instance: Instance, visitContext: VisitHistory.VisitContext?) {\n        self._instance = .init(wrappedValue: instance)\n        self._communityLoader = .init(wrappedValue: .init(\n            api: .getApiClient(url: instance.actorId.hostUrl, username: nil),\n            hostApi: instance.api\n        ))\n        self.visitContext = visitContext\n    }\n    \n    var body: some View {\n        content\n            .animation(.easeOut(duration: 0.2), value: instance.apiIsLocal)\n            .task { await refresh() }\n            .onAppear { logVisit(instance) }\n            .navigationBarTitleDisplayMode(.inline)\n            .themedGroupedBackground()\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        FancyScrollView {\n            ProfileHeaderView(\n                instance,\n                fallback: .instanceAvatar,\n                blockedOverride: (appState.firstSession as? UserSession)?.blocks?.contains(instance)\n            )\n            .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n            if instance.apiIsLocal {\n                BubblePicker(\n                    availableTabs,\n                    selected: $selectedTab,\n                    label: { $0.label }\n                )\n                switch selectedTab {\n                case .about:\n                    aboutTab\n                case .communities:\n                    InstanceCommunityListView(\n                        communityLoader: communityLoader,\n                        errorDetails: $communityListErrorDetails\n                    )\n                case .details:\n                    InstanceDetailsView(instance: instance)\n                case .administration:\n                    administrationTab\n                case .safety:\n                    safetyTab\n                        .onAppear(perform: attemptToLoadFediseerData)\n                }\n            } else {\n                ProgressView()\n                    .tint(.themedSecondary)\n                    .padding(.top)\n            }\n        }\n        .toolbar {\n            ToolbarEllipsisMenu(instance: instance)\n        }\n    }\n    \n    @ViewBuilder\n    var administrationTab: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            if instance.api.supports(.modlog, defaultValue: true) {\n                ModlogButtonView(instance: instance)\n            }\n            \n            ExpectedView(instance.administrators) { administrators in\n                VStack(spacing: Constants.main.halfSpacing) {\n                    ForEach(administrators) { person in\n                        PersonListRow(person)\n                            .quickSwipes(administratorQuickSwipes(person: person))\n                    }\n                }\n            }\n            \n            if appState.firstApi.isAdmin {\n                Button(\"Add Administrator\", icon: .general.add, action: openAddAdminSheet)\n                    .buttonStyle(.capsule)\n                    .padding(.bottom, Constants.main.halfSpacing)\n                    .confirmationDialog(\"Add Administrator\", isPresented: $showingConfirmation) {\n                        Button(\"Yes\", action: addNewAdmin)\n                    } message: {\n                        if let displayName = newAdmin?.displayName {\n                            Text(\"Really appoint \\(displayName) as an administrator of \\(instance.displayName)?\")\n                        } else {\n                            Text(\"Really appoint this user as an administrator of \\(instance.displayName)?\")\n                        }\n                    }\n            }\n        }\n        .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    var safetyTab: some View {\n        if let fediseerData {\n            InstanceSafetyView(instance: instance, fediseerData: fediseerData)\n        } else {\n            ProgressView()\n                .padding(.top, 30)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Instance/UptimeData.swift",
    "content": "//\n//  UptimeData.swift\n//  Mlem\n//\n//  Created by Sjmarf on 28/01/2024.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct UptimeData: Codable, Hashable {\n    let results: [UptimeResponseTime]\n    let events: [UptimeEvent]\n    \n    var downtimes: [DowntimePeriod] {\n        var ret: [DowntimePeriod] = []\n        var previous: UptimeEvent?\n        for event in events {\n            if event.type == .healthy {\n                if let previous {\n                    ret.append(.init(startTime: previous.timestamp, endTime: event.timestamp))\n                }\n            }\n            previous = event\n        }\n        return ret.reversed()\n    }\n}\n\nstruct UptimeResponseTime: Codable, Identifiable, Hashable {\n    let success: Bool\n    let duration: Int\n    let timestamp: Date\n    \n    var durationMs: Int {\n        duration / 1_000_000\n    }\n    \n    var id: Int { Int(timestamp.timeIntervalSince1970) }\n}\n\nstruct UptimeEvent: Codable, Identifiable, Hashable {\n    enum EventType: String, Codable {\n        case start = \"START\"\n        case healthy = \"HEALTHY\"\n        case unhealthy = \"UNHEALTHY\"\n    }\n    \n    let type: EventType\n    let timestamp: Date\n    \n    var id: Int { Int(timestamp.timeIntervalSince1970) }\n}\n\nstruct DowntimePeriod: Codable, Identifiable {\n    let startTime: Date\n    let endTime: Date\n    \n    var id: Int { Int(startTime.timeIntervalSince1970) }\n    \n    var duration: TimeInterval {\n        endTime.timeIntervalSince(startTime)\n    }\n    \n    var severityColor: Color {\n        if duration < 60 * 5 {\n            .secondary\n        } else if duration < 60 * 30 {\n            .orange\n        } else {\n            .red\n        }\n    }\n    \n    func differenceTitle(unitsStyle: DateComponentsFormatter.UnitsStyle = .short) -> String {\n        let formatter = DateComponentsFormatter()\n        formatter.unitsStyle = unitsStyle\n        formatter.maximumUnitCount = 2\n        return formatter.string(from: duration) ?? \"Unknown\"\n    }\n    \n    var relativeTimeCaption: String {\n        endTime.getRelativeTime()\n    }\n    \n    private var timeOnlyFormatter: DateFormatter {\n        let formatter = DateFormatter()\n        formatter.timeStyle = .short\n        formatter.dateStyle = .none\n        return formatter\n    }\n    \n    private var dateAndTimeFormatter: DateFormatter {\n        let formatter = DateFormatter()\n        formatter.dateStyle = .short\n        formatter.timeStyle = .short\n        return formatter\n    }\n    \n    var differenceCaption: String {\n        if duration < 60 {\n            let formatter = DateFormatter()\n            formatter.dateStyle = .short\n            formatter.timeStyle = .short\n            return formatter.string(from: startTime)\n        }\n        \n        let onSameDay = Calendar.current.isDate(startTime, equalTo: endTime, toGranularity: .day)\n        \n        if onSameDay {\n            return \"\\(dateAndTimeFormatter.string(from: startTime)) to \\(timeOnlyFormatter.string(from: endTime))\"\n        }\n        \n        return \"\\(dateAndTimeFormatter.string(from: startTime)) to \\(dateAndTimeFormatter.string(from: endTime))\"\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/MessageFeedView/MessageBubbleView.swift",
    "content": "//\n//  MessageBubbleView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-22.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct MessageBubbleView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.palette) var palette\n    \n    let message: any Message\n    \n    var body: some View {\n        Group {\n            let blocks: [BlockNode] = .init(message.content)\n            if blocks.isSimpleParagraphs {\n                MarkdownText(\n                    blocks, configuration: message.isOwnMessage ? .inverted(palette: palette) : .default(palette: palette)\n                )\n            } else {\n                Markdown(\n                    blocks, configuration: message.isOwnMessage ? .inverted(palette: palette) : .default(palette: palette)\n                )\n            }\n        }\n        .tint(message.isOwnMessage ? palette.contrastingLabel.opacity(0.6) : palette.accent)\n        .padding(Constants.main.standardSpacing)\n        .padding(message.isOwnMessage ? .trailing : .leading, 7)\n        .padding(message.isOwnMessage ? .leading : .trailing, 2)\n        .background(\n            message.isOwnMessage ? .themedAccent : .themedSecondaryGroupedBackground,\n            in: BubbleShape(myMessage: message.isOwnMessage)\n        )\n        .contentShape(.contextMenuPreview, BubbleShape(myMessage: message.isOwnMessage))\n        .contextMenu(message: message)\n    }\n}\n\nprivate struct BubbleShape: Shape {\n    var myMessage: Bool\n    \n    // swiftlint:disable:next function_body_length\n    func path(in rect: CGRect) -> Path {\n        let width = rect.width\n        let height = rect.height\n        \n        let bezierPath = UIBezierPath()\n        if !myMessage {\n            bezierPath.move(to: CGPoint(x: 20, y: height))\n            bezierPath.addLine(to: CGPoint(x: width - 15, y: height))\n            bezierPath.addCurve(\n                to: CGPoint(x: width, y: height - 15),\n                controlPoint1: CGPoint(x: width - 8, y: height),\n                controlPoint2: CGPoint(x: width, y: height - 8)\n            )\n            bezierPath.addLine(to: CGPoint(x: width, y: 15))\n            bezierPath.addCurve(\n                to: CGPoint(x: width - 15, y: 0),\n                controlPoint1: CGPoint(x: width, y: 8),\n                controlPoint2: CGPoint(x: width - 8, y: 0)\n            )\n            bezierPath.addLine(to: CGPoint(x: 20, y: 0))\n            bezierPath.addCurve(\n                to: CGPoint(x: 5, y: 15),\n                controlPoint1: CGPoint(x: 12, y: 0),\n                controlPoint2: CGPoint(x: 5, y: 8)\n            )\n            bezierPath.addLine(to: CGPoint(x: 5, y: height - 10))\n            bezierPath.addCurve(\n                to: CGPoint(x: 0, y: height),\n                controlPoint1: CGPoint(x: 5, y: height - 1),\n                controlPoint2: CGPoint(x: 0, y: height)\n            )\n            bezierPath.addLine(to: CGPoint(x: -1, y: height))\n            bezierPath.addCurve(\n                to: CGPoint(x: 12, y: height - 4),\n                controlPoint1: CGPoint(x: 4, y: height + 1),\n                controlPoint2: CGPoint(x: 8, y: height - 1)\n            )\n            bezierPath.addCurve(\n                to: CGPoint(x: 20, y: height),\n                controlPoint1: CGPoint(x: 15, y: height),\n                controlPoint2: CGPoint(x: 20, y: height)\n            )\n        } else {\n            bezierPath.move(to: CGPoint(x: width - 20, y: height))\n            bezierPath.addLine(to: CGPoint(x: 15, y: height))\n            bezierPath.addCurve(\n                to: CGPoint(x: 0, y: height - 15),\n                controlPoint1: CGPoint(x: 8, y: height),\n                controlPoint2: CGPoint(x: 0, y: height - 8)\n            )\n            bezierPath.addLine(to: CGPoint(x: 0, y: 15))\n            bezierPath.addCurve(\n                to: CGPoint(x: 15, y: 0),\n                controlPoint1: CGPoint(x: 0, y: 8),\n                controlPoint2: CGPoint(x: 8, y: 0)\n            )\n            bezierPath.addLine(to: CGPoint(x: width - 20, y: 0))\n            bezierPath.addCurve(\n                to: CGPoint(x: width - 5, y: 15),\n                controlPoint1: CGPoint(x: width - 12, y: 0),\n                controlPoint2: CGPoint(x: width - 5, y: 8)\n            )\n            bezierPath.addLine(to: CGPoint(x: width - 5, y: height - 12))\n            bezierPath.addCurve(\n                to: CGPoint(x: width, y: height),\n                controlPoint1: CGPoint(x: width - 5, y: height - 1),\n                controlPoint2: CGPoint(x: width, y: height)\n            )\n            bezierPath.addLine(to: CGPoint(x: width + 1, y: height))\n            bezierPath.addCurve(\n                to: CGPoint(x: width - 12, y: height - 4),\n                controlPoint1: CGPoint(x: width - 4, y: height + 1),\n                controlPoint2: CGPoint(x: width - 8, y: height - 1)\n            )\n            bezierPath.addCurve(\n                to: CGPoint(x: width - 20, y: height),\n                controlPoint1: CGPoint(x: width - 15, y: height),\n                controlPoint2: CGPoint(x: width - 20, y: height)\n            )\n        }\n        return Path(bezierPath.cgPath)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/MessageFeedView/MessageFeedView+Logic.swift",
    "content": "//\n//  MessageFeedView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-23.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nextension MessageFeedView {\n    var shouldDelayBecomeFirstResponder: Bool {\n        // Only delay the keyboard opening if being pushed onto the navigation stack rather than opening in sheet\n        !navigation.isAtRoot\n    }\n    \n    func sendMessage(_ scrollProxy: ScrollViewProxy) async {\n        self.isSending = true\n        defer { self.isSending = false }\n        do {\n            guard !textView.text.isEmpty else { return }\n            let message = try await appState.firstApi.createMessage(personId: person.id, content: textView.text)\n            withAnimation {\n                feedLoader?.prependItem(message)\n                scrollProxy.scrollTo(message.id, anchor: .bottom)\n            }\n            textView.text = \"\"\n        } catch {\n            handleError(error)\n        }\n    }\n    \n    func editMessage(_ message: any Message) async {\n        do {\n            try await message.edit(content: textView.text)\n            editing = nil\n            textView.text = \"\"\n            textView.resignFirstResponder()\n        } catch {\n            handleError(error)\n        }\n    }\n    \n    func messageIsFirstOfDay(_ message: Message2) -> Bool {\n        guard let feedLoader else { return false }\n        guard let index = feedLoader.items.firstIndex(of: message) else {\n            assertionFailure()\n            return false\n        }\n        guard index < feedLoader.items.count - 1 else { return true }\n        let previousMessage = feedLoader.items[index + 1]\n        return !Calendar.current.isDate(previousMessage.created, inSameDayAs: message.created)\n    }\n    \n    var minTextEditorHeight: CGFloat {\n        Constants.main.standardSpacing * 2 + UIFont.preferredFont(forTextStyle: .body).lineHeight\n    }\n    \n    func messageFooterText(for message: Message2) -> String? {\n        var parts: [String] = .init()\n        if message == feedLoader?.items.first, Calendar.current.isDateInToday(message.created) {\n            parts.append(message.created.formatted(date: .omitted, time: .shortened))\n        }\n        if message.updated != nil {\n            parts.append(.init(localized: \"Edited\"))\n        }\n        return parts.joined(separator: \" • \")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/MessageFeedView/MessageFeedView.swift",
    "content": "//\n//  MessageFeedView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-22.\n//\n\nimport Actions\nimport ComponentViews\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\n// swiftlint:disable:next type_body_length\nstruct MessageFeedView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.dismiss) var dismiss\n    \n    let person: Person\n    let focusTextField: Bool\n    @State var editing: (any Message)?\n    \n    /// Tracks whether the text view was firstResponder when a sheet was opened. Nil when no sheet is open.\n    @State var textViewWasFirstResponder: Bool?\n    \n    let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()\n    \n    @State var markdownToolbarEditorModel: MarkdownEditorToolbarModel = .init()\n    \n    @ScaledMetric(relativeTo: .body) var sendButtonHeight = 28\n\n    init(\n        person: Person,\n        messageContent: String = \"\",\n        focusTextField: Bool,\n        editing: (any Message)?\n    ) {\n        self.person = person\n        self.focusTextField = focusTextField\n        self._editing = .init(wrappedValue: editing)\n        let textView = UITextView()\n        textView.text = editing?.content ?? messageContent\n        _textView = .init(wrappedValue: textView)\n    }\n    \n    @State var feedLoader: MessageFeedLoader?\n    @State var textView: UITextView = .init()\n    \n    @State var uploadHistory: ImageUploadHistoryManager = .init()\n\n    @State var isSending: Bool = false\n        \n    var body: some View {\n        content(person: person)\n            .navigationTitle(person.displayName)\n            .navigationBarTitleDisplayMode(.inline)\n            .toolbar {\n                if navigation.isInsideSheet {\n                    ToolbarItem(placement: .topBarTrailing) {\n                        CloseButtonView()\n                    }\n                } else {\n                    ToolbarItem(placement: .principal) { navigationTitleView(person: person) }\n                    ToolbarItemGroup(placement: .secondaryAction) {\n                        SwiftUI.Section {\n                            ActionButtons(person: person)\n                        }\n                    }\n                }\n            }\n            .popupAnchor()\n    }\n    \n    // swiftlint:disable:next function_body_length\n    @ViewBuilder func content(person: Person) -> some View {\n        ScrollViewReader { scrollProxy in\n            ScrollView {\n                if let feedLoader {\n                    LazyVStack(spacing: 0) {\n                        ForEach(feedLoader.items.reversed()) { message in\n                            if !message.deleted {\n                                bubbleView(message: message, feedLoader: feedLoader)\n                                    .id(message.id)\n                            }\n                        }\n                    }\n                    .scrollTargetLayout()\n                    .padding(.top, 50)\n                    .onReceive(timer) { _ in\n                        Task { @MainActor in\n                            do {\n                                try await feedLoader.refresh(clearBeforeRefresh: false)\n                            } catch {\n                                handleError(error)\n                            }\n                        }\n                    }\n                }\n            }\n            .safeAreaBar_(edge: .bottom) {\n                if #available(iOS 26.0, *) {\n                    textInput(scrollProxy)\n                        .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 24))\n                        .padding(.horizontal, textView.isFirstResponder ? Constants.main.standardSpacing : Constants.main.doubleSpacing)\n                        .padding(.bottom, 25)\n                        .padding(.top, Constants.main.standardSpacing)\n                } else {\n                    textInput(scrollProxy)\n                        .background(\n                            RoundedRectangle(cornerRadius: Constants.main.doubleSpacing)\n                                .strokeBorder(.themedTertiary.opacity(0.5), lineWidth: 1)\n                        )\n                        .padding(Constants.main.standardSpacing)\n                        .background(.bar)\n                }\n            }\n            .defaultScrollAnchor(.bottom)\n            .scrollDismissesKeyboard(.interactively)\n            .themedGroupedBackground()\n            .onAppear {\n                if feedLoader == nil {\n                    feedLoader = .init(person: person, pageSize: 50)\n                    Task { @MainActor in\n                        do {\n                            try await feedLoader?.loadMoreItems()\n                        } catch {\n                            handleError(error)\n                        }\n                    }\n                }\n            }\n            .onChange(of: navigation.model?.layers.count) {\n                if let numLayers = navigation.model?.layers.count {\n                    if numLayers > 0, textViewWasFirstResponder == nil {\n                        textViewWasFirstResponder = textView.isFirstResponder\n                        textView.resignFirstResponder()\n                    } else if textViewWasFirstResponder ?? false {\n                        textViewWasFirstResponder = nil\n                        textView.becomeFirstResponder()\n                    }\n                }\n            }\n            .environment(\\.isInMessageFeed, true)\n            .environment(\\.editMessage) { message in\n                editing = message\n                textView.text = message.content\n                textView.becomeFirstResponder()\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func bubbleView(message: Message2, feedLoader: MessageFeedLoader) -> some View {\n        if messageIsFirstOfDay(message) {\n            Text(message.created.messagesRelativeDate())\n                .font(.footnote)\n                .foregroundStyle(.themedSecondary)\n                .padding(.bottom, Constants.main.halfSpacing)\n        }\n        VStack(alignment: message.isOwnMessage ? .trailing : .leading, spacing: Constants.main.halfSpacing) {\n            MessageBubbleView(message: message)\n                .padding(message.isOwnMessage ? .leading : .trailing, 50)\n                .frame(maxWidth: 400, alignment: message.isOwnMessage ? .trailing : .leading)\n                .onAppear {\n                    do {\n                        try feedLoader.loadIfThreshold(message)\n                    } catch {\n                        handleError(error)\n                    }\n                }\n            if let footerText = messageFooterText(for: message) {\n                Text(footerText)\n                    .font(.footnote)\n                    .foregroundStyle(.themedSecondary)\n                    .padding(.horizontal, Constants.main.halfSpacing)\n            }\n        }\n        .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    func textInput(_ scrollProxy: ScrollViewProxy) -> some View {\n        OptimalHeightLayout {\n            HStack(alignment: .bottom) {\n                ScrollView { textInputView(scrollProxy) }\n                    .scrollBounceBehavior(.basedOnSize, axes: .vertical)\n                    .scrollIndicators(.hidden)\n                HStack(spacing: 6) {\n                    if editing != nil {\n                        cancelEditButton()\n                    }\n                    sendButton(scrollProxy)\n                }\n                .frame(height: minTextEditorHeight - 12)\n                .padding(6)\n                .fontWeight(.semibold)\n            }\n            .frame(minHeight: minTextEditorHeight, maxHeight: 200)\n            .padding(UIDevice.isIos26 ? 2 : 0)\n        }\n    }\n    \n    @ViewBuilder\n    func cancelEditButton() -> some View {\n        Button {\n            editing = nil\n            textView.text = \"\"\n            textView.resignFirstResponder()\n        } label: {\n            textInputButtonLabel(icon: .general.close)\n        }\n        .tint(.themedTertiary)\n    }\n    \n    @ViewBuilder\n    func sendButton(_ scrollProxy: ScrollViewProxy) -> some View {\n        Button {\n            Task { @MainActor in\n                if let editing {\n                    await editMessage(editing)\n                } else {\n                    await sendMessage(scrollProxy)\n                }\n            }\n        } label: {\n            textInputButtonLabel(icon: editing == nil ? .lemmy.sendMessage : .general.success)\n        }\n        .tint(.themedAccent)\n        .compositingGroup()\n        .opacity(isSending ? 0.4 : 1)\n        .disabled(isSending)\n    }\n    \n    @ViewBuilder\n    func textInputButtonLabel(icon: Icon) -> some View {\n        if UIDevice.isIos26 {\n            Image(icon: icon)\n                .foregroundStyle(.themedContrastingLabel)\n                .frame(height: sendButtonHeight)\n                .padding(.horizontal, 12)\n                .background(.tint, in: .capsule)\n                .frame(height: minTextEditorHeight - 12)\n                .padding(.bottom, 1)\n        } else {\n            Image(icon: icon)\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .frame(maxHeight: .infinity)\n                .foregroundStyle(.themedContrastingLabel, .tint)\n                .symbolVariant(.circle.fill)\n        }\n    }\n    \n    @ViewBuilder\n    func textInputView(_ scrollProxy: ScrollViewProxy) -> some View {\n        MarkdownTextEditor(\n            onBeginEditing: {\n                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {\n                    withAnimation {\n                        scrollProxy.scrollTo(feedLoader?.items.first?.id)\n                    }\n                }\n            },\n            prompt: \"Send a Message...\",\n            textView: textView,\n            insets: .init(\n                top: Constants.main.standardSpacing,\n                left: Constants.main.standardSpacing,\n                bottom: Constants.main.standardSpacing,\n                right: Constants.main.standardSpacing\n            ),\n            firstResponder: focusTextField && !shouldDelayBecomeFirstResponder,\n            sizingOffset: 5,\n            content: {\n                MarkdownEditorToolbarView(\n                    textView: textView,\n                    uploadHistory: uploadHistory,\n                    model: markdownToolbarEditorModel\n                )\n            }\n        )\n        .onChange(of: appState.firstApi) {\n            markdownToolbarEditorModel.imageUploadApi = appState.firstApi\n        }\n        .frame(\n            maxWidth: .infinity,\n            minHeight: minTextEditorHeight\n        )\n        .onAppear {\n            if focusTextField, shouldDelayBecomeFirstResponder {\n                DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {\n                    textView.becomeFirstResponder()\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func navigationTitleView(person: Person) -> some View {\n        NavigationLink(.person(person)) {\n            HStack(spacing: Constants.main.halfSpacing) {\n                CircleCroppedImageView(person, frame: 24)\n                Text(person.displayName)\n                    .foregroundStyle(.themedPrimary)\n                    .font(.headline)\n                Image(icon: .general.forward)\n                    .imageScale(.small)\n                    .fontWeight(.semibold)\n                    .foregroundStyle(.themedTertiary)\n            }\n        }\n    }\n}\n\nextension EnvironmentValues {\n    @Entry var editMessage: ((Message2) -> Void)?\n    @Entry var isInMessageFeed: Bool = false\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Modlog/ModlogEntryView.swift",
    "content": "//\n//  ModlogEntryView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-25.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ModlogEntryView: View {\n    @Environment(\\.palette) var palette\n    \n    let entry: ModlogEntry\n    var targetCommunity: Community?\n    @State private var id = UUID()\n    \n    var body: some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            headerView\n            contentView\n            HStack(spacing: 5) {\n                Image(icon: .general.time)\n                Text(entry.created.formatted(date: .abbreviated, time: .shortened))\n            }\n            .font(.footnote)\n            .foregroundStyle(.themedSecondary)\n        }\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .padding(Constants.main.standardSpacing)\n        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        .environment(\\.communityContext, entry.type.community)\n    }\n    \n    @ViewBuilder\n    var headerView: some View {\n        HStack(spacing: Constants.main.standardSpacing) {\n            Circle()\n                .fill(entry.type.color.opacity(0.3))\n                .frame(width: 24, height: 24)\n                .overlay {\n                    Image(icon: entry.type.icon)\n                        .foregroundStyle(entry.type.color)\n                }\n            Text(headerText)\n                .font(.footnote)\n                .foregroundStyle(.secondary)\n        }\n        .imageScale(.small)\n        .symbolVariant(.fill)\n    }\n    \n    var headerText: LocalizedStringKey {\n        if let moderator = entry.moderator {\n            let userText = moderator.nameTextView(\n                showFlairs: true,\n                showInstance: true,\n                communityContext: targetCommunity ?? entry.type.community,\n                font: .footnote,\n                palette: palette\n            )\n            return entry.type.label(userText: userText)\n        }\n        return entry.type.label(userText: nil)\n    }\n    \n    @ViewBuilder\n    var contentView: some View {\n        switch entry.type {\n        case let .removePost(post, community: community, removed: _, reason: reason):\n            reasonView(reason)\n            postLink(post: post, community: community)\n        case let .lockPost(post, community: community, locked: _):\n            postLink(post: post, community: community)\n        case let .pinPost(post, community: community, pinned: _, type: _):\n            postLink(post: post, community: community)\n        case let .purgePost(reason: reason):\n            reasonView(reason)\n        case let .removeComment(comment, creator: _, post: _, community: _, removed: _, reason: reason):\n            reasonView(reason)\n            commentLink(comment: comment)\n        case let .purgeComment(reason: reason):\n            reasonView(reason)\n        case let .removeCommunity(community, removed: _, reason: reason):\n            reasonView(reason)\n            FullyQualifiedLinkView(community, labelStyle: .medium)\n        case let .purgeCommunity(reason: reason):\n            reasonView(reason)\n        case let .hideCommunity(community, hidden: _, reason: reason):\n            reasonView(reason)\n            FullyQualifiedLinkView(community, labelStyle: .medium)\n        case let .transferCommunityOwnership(person: person, community: community):\n            transferCommunityView(person: person, community: community)\n        case let .updatePersonModeratorStatus(person: person, community: community, appointed: appointed):\n            updatePersonModeratorStatusView(person: person, community: community, appointed: appointed)\n        case let .updatePersonAdminStatus(person: person, appointed: appointed):\n            updatePersonModeratorStatusView(person: person, community: nil, appointed: appointed)\n        case let .banPersonFromCommunity(person: person, community: community, banned: banned, reason: reason, expires: expires):\n            reasonView(reason)\n            banPersonView(person: person, community: community, banned: banned, expires: expires)\n        case let .banPersonFromInstance(person: person, banned: banned, reason: reason, expires: expires):\n            reasonView(reason)\n            banPersonView(person: person, community: nil, banned: banned, expires: expires)\n        case let .purgePerson(reason: reason):\n            reasonView(reason)\n        }\n    }\n    \n    @ViewBuilder\n    func banPersonView(person: Person, community: Community?, banned: Bool, expires: Date?) -> some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            let userText = person.nameTextView(\n                showFlairs: true,\n                showInstance: true,\n                communityContext: targetCommunity ?? community,\n                font: .subheadline,\n                palette: palette\n            )\n            let targetText: Text\n            if let community {\n                targetText = community.nameTextView(\n                    showFlairs: true,\n                    showInstance: true,\n                    font: .subheadline,\n                    palette: palette\n                )\n            } else {\n                targetText = Text(\"Instance\")\n            }\n            if banned {\n                let expiresText = expires?.formatted(date: .abbreviated, time: .omitted) ?? \"Never\"\n                return Text(\"Banned: \\(userText)\\nFrom: \\(targetText)\\nExpires: \\(expiresText)\")\n            } else {\n                return Text(\"Unbanned: \\(userText)\\nFrom: \\(targetText)\")\n            }\n        }\n        .imageScale(.small)\n        .symbolVariant(.fill)\n        .foregroundStyle(.themedSecondary)\n        .font(.subheadline)\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .padding(Constants.main.standardSpacing)\n        .background(.themedTertiaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    func transferCommunityView(\n        person: Person,\n        community: Community\n    ) -> some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            let userText = person.nameTextView(\n                showFlairs: true,\n                showInstance: true,\n                communityContext: targetCommunity ?? community,\n                font: .subheadline,\n                palette: palette\n            )\n            let communityText = community.nameTextView(\n                showFlairs: true,\n                showInstance: true,\n                font: .subheadline,\n                palette: palette\n            )\n            Text(\"Community: \\(communityText)\\nNew Owner: \\(userText)\")\n                .imageScale(.small)\n        }\n        .foregroundStyle(.themedSecondary)\n        .font(.subheadline)\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .padding(Constants.main.standardSpacing)\n        .background(.themedTertiaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    func updatePersonModeratorStatusView(\n        person: Person,\n        community: Community?,\n        appointed: Bool\n    ) -> some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            let userText = person.nameTextView(\n                showFlairs: true,\n                showInstance: true,\n                communityContext: targetCommunity ?? community,\n                font: .subheadline,\n                palette: palette\n            )\n            if let community {\n                let communityText = community.nameTextView(\n                    showFlairs: true,\n                    showInstance: true,\n                    font: .subheadline,\n                    palette: palette\n                )\n                Text(\n                    appointed ? \"Appointed: \\(userText)\\nTo: \\(communityText)\" : \"Removed: \\(userText)\\nFrom: \\(communityText)\"\n                )\n            } else {\n                Text(appointed ? \"Appointed: \\(userText)\" : \"Removed: \\(userText)\")\n            }\n        }\n        .foregroundStyle(.themedSecondary)\n        .imageScale(.small)\n        .symbolVariant(.fill)\n        .font(.subheadline)\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .padding(Constants.main.standardSpacing)\n        .background(.themedTertiaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    func reasonView(_ reason: String?) -> some View {\n        if let reason {\n            Text(\"Reason:\").foregroundStyle(.secondary) + Text(verbatim: \" \\(reason)\")\n        } else {\n            Text(\"No reason given\")\n                .foregroundStyle(.secondary)\n                .italic()\n        }\n    }\n    \n    @ViewBuilder\n    func postLink(post: Post, community: Community) -> some View {\n        NavigationLink(.post(post)) {\n            FooterLinkView(title: post.title, subtitle: community.fullNameWithPrefix)\n        }\n        .id(\"\\(id)_modlog_footer\")\n    }\n    \n    @ViewBuilder\n    func commentLink(comment: Comment) -> some View {\n        NavigationLink(.comment(comment, exposeRemovedContent: true)) {\n            VStack {\n                Text(comment.content)\n                    .frame(maxWidth: .infinity, alignment: .leading)\n                    .font(.subheadline)\n                    .fontWeight(.semibold)\n                    .multilineTextAlignment(.leading)\n                    .lineLimit(5)\n            }\n            .foregroundStyle(.themedSecondary)\n            .padding(Constants.main.standardSpacing)\n            .background(.themedTertiaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n            .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        }\n        .id(\"\\(id)_modlog_footer\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Modlog/ModlogView+Filters.swift",
    "content": "//\n//  ModlogView+Filters.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-11-22.\n//  \n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\n\nextension ModlogView {\n    @ViewBuilder\n    func filtersView(communityFilter: CommunityFilter) -> some View {\n        ScrollView(.horizontal) {\n            HStack {\n                typeFilterView()\n                    .buttonStyle(.feedFilter(isOn: actionTypeFilter != nil))\n                communityFilterView(communityFilter: communityFilter)\n                personFilterView(filter: $targetPersonFilter, icon: .lemmy.targetedPerson)\n                personFilterView(filter: $moderatorPersonFilter, icon: .lemmy.moderation)\n            }\n            .padding(.horizontal, Constants.main.standardSpacing)\n        }\n        .scrollIndicators(.hidden)\n    }\n\n    @ViewBuilder\n    func communityFilterView(communityFilter: CommunityFilter) -> some View {\n        Button {\n            if communityFilter == .any {\n                navigation.openSheet(.communityPicker(api: api) { community in\n                    self.communityFilter = .community(community)\n                })\n            } else {\n                self.communityFilter = .any\n            }\n        } label: {\n            Label(communityFilter.label, icon: .lemmy.community)\n        }\n        .buttonStyle(\n            .feedFilter(\n                isOn: communityFilter != .any,\n                icon: communityFilter == .any ? .general.dropDown : .general.close\n            )\n        )\n    }\n    \n    @ViewBuilder\n    func personFilterView(filter: Binding<PersonFilter>, icon: Icon) -> some View {\n        Button {\n            if filter.wrappedValue == .any {\n                navigation.openSheet(.personPicker(api: api) { person in\n                    filter.wrappedValue = .person(person)\n                })\n            } else {\n                filter.wrappedValue = .any\n            }\n        } label: {\n            Label(filter.wrappedValue.label, icon: icon)\n        }\n        .buttonStyle(\n            .feedFilter(\n                isOn: filter.wrappedValue != .any,\n                icon: filter.wrappedValue == .any ? .general.dropDown : .general.close\n            )\n        )\n    }\n    \n    @ViewBuilder\n    func typeFilterView() -> some View {\n        Menu(\n            String(localized: actionTypeFilter?.label ?? \"Action Type\"),\n            icon: actionTypeFilter?.icon ?? .general.action\n        ) {\n            Section {\n                Toggle(\n                    \"Any\",\n                    icon: .general.action,\n                    isOn: .init(get: { actionTypeFilter == nil }, set: { _ in actionTypeFilter = nil })\n                )\n            }\n            Section {\n                Picker(\"Post\", icon: .lemmy.post, selection: $actionTypeFilter) {\n                    typeFilterLabel(.removePost)\n                    typeFilterLabel(.lockPost)\n                    typeFilterLabel(.pinPost)\n                    typeFilterLabel(.purgePost)\n                }\n                Picker(\"Comment\", icon: .lemmy.comment, selection: $actionTypeFilter) {\n                    typeFilterLabel(.removeComment)\n                    typeFilterLabel(.purgeComment)\n                }\n                Picker(\"Community\", icon: .lemmy.community, selection: $actionTypeFilter) {\n                    typeFilterLabel(.removeCommunity)\n                    typeFilterLabel(.hideCommunity)\n                    typeFilterLabel(.updatePersonModeratorStatus)\n                    typeFilterLabel(.transferCommunityOwnership)\n                    typeFilterLabel(.purgeCommunity)\n                }\n                Picker(\"User\", icon: .lemmy.person, selection: $actionTypeFilter) {\n                    typeFilterLabel(.banPersonFromInstance)\n                    typeFilterLabel(.banPersonFromCommunity)\n                    typeFilterLabel(.updatePersonModeratorStatus)\n                    typeFilterLabel(.updatePersonAdminStatus)\n                    typeFilterLabel(.purgePerson)\n                }\n            }\n        }\n        .pickerStyle(.menu)\n    }\n    \n    @ViewBuilder\n    func typeFilterLabel(_ type: ModlogEntryType) -> some View {\n        if type.appliesToCommunity || communityFilter == .any {\n            Label(type.contextualLabel.key, icon: type.icon)\n                .tag(type)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Modlog/ModlogView+Logic.swift",
    "content": "//\n//  ModlogView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-11.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension ModlogView {\n    enum InitialTarget: Hashable {\n        case community(Community)\n        case instance(Instance)\n        case currentInstance\n    }\n    \n    enum CommunityFilter: Hashable {\n        case any\n        case community(Community)\n        \n        var label: String {\n            switch self {\n            case .any: .init(localized: \"Any Community\")\n            case let .community(community): community.name\n            }\n        }\n        \n        var communityValue: Community? {\n            switch self {\n            case let .community(community): community\n            default: nil\n            }\n        }\n        \n        static func == (lhs: CommunityFilter, rhs: CommunityFilter) -> Bool {\n            switch (lhs, rhs) {\n            case let (.community(lhs), .community(rhs)): lhs === rhs\n            case (.any, .any): true\n            default: false\n            }\n        }\n\n        func hash(into hasher: inout Hasher) {\n            hasher.combine(communityValue?.api.cacheId)\n            hasher.combine(communityValue?.id)\n        }\n    }\n\n    enum PersonFilter: Hashable {\n        case any\n        case person(Person)\n\n        var label: String {\n            switch self {\n            case .any: .init(localized: \"Any User\")\n            case let .person(person): person.name\n            }\n        }\n\n        var personValue: (Person)? {\n            switch self {\n            case let .person(person): person\n            default: nil\n            }\n        }\n\n        static func == (lhs: Self, rhs: Self) -> Bool {\n            switch (lhs, rhs) {\n            case let (.person(lhs), .person(rhs)): lhs === rhs\n            case (.any, .any): true\n            default: false\n            }\n        }\n\n        func hash(into hasher: inout Hasher) {\n            hasher.combine(personValue?.api.cacheId)\n            hasher.combine(personValue?.id)\n        }\n    }\n    \n    func refresh() async throws {\n        try await feedLoader.refresh(\n            api: api,\n            communityId: communityFilter?.communityValue?.id,\n            targetPersonId: targetPersonFilter.personValue?.id,\n            moderatorPersonId: moderatorPersonFilter.personValue?.id,\n            clearBeforeRefresh: true\n        )\n    }\n    \n    var activeFeedLoader: any FeedLoading<ModlogEntry> {\n        if let actionTypeFilter {\n            feedLoader.childLoader(ofType: actionTypeFilter)\n        } else {\n            feedLoader\n        }\n    }\n\n    var refreshHashValue: Int {\n        var hasher = Hasher()\n        hasher.combine(communityFilter)\n        hasher.combine(targetPersonFilter)\n        hasher.combine(moderatorPersonFilter)\n        return hasher.finalize()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Modlog/ModlogView.swift",
    "content": "//\n//  ModlogView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-25.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct ModlogView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    @Setting(\\.safety_enableModlogWarning) var showModlogWarning\n    \n    let api: ApiClient\n    let initialTarget: InitialTarget\n    \n    @State var feedLoader: ModlogFeedLoader\n    @State var warningPresented: Bool = Settings.get(\\.safety_enableModlogWarning)\n    \n    @State var communityFilter: CommunityFilter?\n    @State var targetPersonFilter: PersonFilter = .any\n    @State var moderatorPersonFilter: PersonFilter = .any\n    @State var actionTypeFilter: ModlogEntryType?\n    \n    init(\n        initialTarget: InitialTarget,\n        targetPerson: Person?,\n        moderatorPerson: Person?\n    ) {\n        self._feedLoader = .init(\n            wrappedValue: .init(\n                api: AppState.main.firstApi,\n                pageSize: Settings.get(\\.behavior_internetSpeed).pageSize,\n                communityId: nil,\n                targetPersonId: nil,\n                moderatorPersonId: nil,\n                sortType: .new\n            )\n        )\n        self.initialTarget = initialTarget\n        switch initialTarget {\n        case let .community(community):\n            self._communityFilter = .init(wrappedValue: .community(community))\n            self.api = community.api\n        case let .instance(instance):\n            self._communityFilter = .init(wrappedValue: .any)\n            self.api = instance.api\n        case .currentInstance:\n            self._communityFilter = .init(wrappedValue: .any)\n            self.api = AppState.main.firstApi\n        }\n        if let person = targetPerson {\n            self._targetPersonFilter = .init(wrappedValue: .person(person))\n        }\n        if let person = moderatorPerson {\n            self._moderatorPersonFilter = .init(wrappedValue: .person(person))\n        }\n    }\n    \n    var body: some View {\n        Group {\n            switch initialTarget {\n            case let .community(initialCommunity):\n                Group {\n                    if let communityFilter {\n                        content(communityFilter: communityFilter)\n                    } else {\n                        ProgressView()\n                            .onAppear {\n                                if communityFilter == nil {\n                                    communityFilter = .community(initialCommunity)\n                                }\n                            }\n                    }\n                }\n            case .instance, .currentInstance:\n                if let communityFilter {\n                    content(communityFilter: communityFilter)\n                } else {\n                    ProgressView()\n                }\n            }\n        }\n        .navigationTitle(\"Modlog\")\n        .navigationBarTitleDisplayMode(.inline)\n        .fullScreenCover(isPresented: $warningPresented) {\n            WarningOverlayView(\n                text: \"The modlog may contain disturbing or adult material.\",\n                isPresented: $warningPresented,\n                showWarningAgain: $showModlogWarning\n            )\n        }\n        .toolbar {\n            if navigation.isInsideSheet {\n                CloseButtonToolbarItem(ios18Label: .xmark)\n            }\n        }\n        .onChange(of: refreshHashValue, initial: true) { oldValue, newValue in\n            // This prevents the feed from refreshing when changing tabs\n            guard oldValue != newValue || feedLoader.loadingState == .initial else {\n                return\n            }\n            if communityFilter != nil {\n                Task {\n                    do {\n                        try await refresh()\n                    } catch {\n                        handleError(error)\n                    }\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func content(communityFilter: CommunityFilter) -> some View {\n        ScrollView {\n            filtersView(communityFilter: communityFilter)\n            LazyVStack(spacing: Constants.main.standardSpacing) {\n                ForEach(\n                    Array(feedLoader.items(ofType: actionTypeFilter).enumerated()),\n                    id: \\.offset\n                ) { _, entry in entryView(entry) }\n                EndOfFeedView(feedLoader: activeFeedLoader, viewType: .hobbit)\n            }\n            .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n        }\n        .themedGroupedBackground()\n    }\n    \n    @ViewBuilder\n    func entryView(_ entry: ModlogEntry) -> some View {\n        ModlogEntryView(entry: entry, targetCommunity: communityFilter?.communityValue)\n            .onAppear {\n                do {\n                    try activeFeedLoader.loadIfThreshold(entry)\n                } catch {\n                    handleError(error)\n                }\n            }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Person/PersonStubResolutionPage.swift",
    "content": "//\n//  PersonStubResolutionView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-02-08.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct PersonStubResolutionPage: View {\n    @Environment(NavigationLayer.self) var navigation\n    \n    let stub: PersonStub\n    let visitContext: VisitHistory.VisitContext\n    \n    @State var upgradeError: Error?\n    \n    var body: some View {\n        content\n            .themedGroupedBackground()\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        if let upgradeError {\n            ErrorView(.init(\n                error: upgradeError,\n                refresh: fetchPost\n            ))\n        } else {\n            ProgressView()\n                .task {\n                    await fetchPost()\n                }\n        }\n    }\n    \n    @discardableResult\n    func fetchPost() async -> Bool {\n        do {\n            let person = try await stub.getPerson()\n            navigation.replace(.person(person, visitContext: visitContext))\n            return true\n        } catch {\n            upgradeError = error\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Person/PersonView+Logic.swift",
    "content": "//\n//  PersonView+Logic.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-19.\n//\n\nimport MlemMiddleware\n\nextension PersonView {\n    func preheatFeedLoader() {\n        Task {\n            guard let feedLoader else { return }\n            do {\n                if feedLoader.loadingState == .initial {\n                    try await feedLoader.loadMoreItems()\n                }\n            } catch {\n                // This is OK to silence because the feed loader will fail when\n                // it appears if this fails, and will show an ErrorView.\n                handleError(error, silent: true)\n            }\n        }\n    }\n    \n    func tabs(person: Person) -> [Tab] {\n        var output: [Tab] = [.overview, .posts, .comments]\n        if !(person.moderatedCommunities.value?.isEmpty ?? true) {\n            output.append(.communities)\n        }\n        return output\n    }\n    \n    func logVisit(_ person: Person) {\n        guard let visitContext else { return }\n        if let session = (appState.firstSession as? UserSession), let visitHistory = session.visitHistory {\n            guard session.api === person.api else { return }\n            visitHistory.addPerson(person, context: visitContext)\n            Task(priority: .background) {\n                try await session.saveVisitHistory()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/Person/PersonView.swift",
    "content": "//\n//  PersonView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/05/2024.\n//\n\nimport Actions\nimport ComponentViews\nimport Flow\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct PersonView: View {\n    enum Tab: String, CaseIterable, Identifiable {\n        case overview, comments, posts, communities\n        \n        var id: Self { self }\n        var label: LocalizedStringResource {\n            switch self {\n            case .overview: \"Overview\"\n            case .comments: \"Comments\"\n            case .posts: \"Posts\"\n            case .communities: \"Communities\"\n            }\n        }\n    }\n    \n    @Setting(\\.post_size) var postSize\n    @Setting(\\.behavior_internetSpeed) var internetSpeed\n    \n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(FiltersTracker.self) var filtersTracker\n    @Environment(\\.palette) var palette\n\n    let visitContext: VisitHistory.VisitContext?\n    \n    @State var person: Person\n    @State private var selectedTab: Tab = .overview\n    @State private var selectedContentType: PersonContentType = .all\n    @State var feedLoader: SingleSourceMixedFeedLoader?\n    @State var isLoading: Bool = false\n\n    let isProfileTab: Bool\n    \n    init(\n        appState: AppState = .main,\n        person: Person,\n        isProfileTab: Bool = false,\n        visitContext: VisitHistory.VisitContext?\n    ) {\n        self.visitContext = visitContext\n        self._person = .init(wrappedValue: person)\n        self.isProfileTab = isProfileTab\n        \n        if person.api === appState.firstApi {\n            self._feedLoader = .init(wrappedValue: .init(\n                api: appState.firstApi,\n                pageSize: internetSpeed.pageSize,\n                userId: person.id,\n                sortType: .new,\n                savedOnly: false,\n                prefetchingConfiguration: .forPostSize(postSize)\n            ))\n        }\n    }\n    \n    var body: some View {\n        content\n            .id(person.uid)\n            .reloadOnAccountSwitch(entity: $person, isLoading: $isLoading) { newPerson in\n                feedLoader = .init(\n                    api: appState.firstApi,\n                    pageSize: internetSpeed.pageSize,\n                    userId: newPerson.id,\n                    sortType: .new,\n                    savedOnly: false,\n                    prefetchingConfiguration: .forPostSize(postSize)\n                )\n            }\n            .onAppear {\n                preheatFeedLoader()\n            }\n            .onChange(of: selectedTab) {\n                switch selectedTab {\n                case .comments: selectedContentType = .comments\n                case .posts: selectedContentType = .posts\n                default: selectedContentType = .all\n                }\n            }\n            .environment(\\.feedContext, .person)\n    }\n    \n    var content: some View {\n        content(person: person)\n            .externalApiWarning(entity: person, isLoading: isLoading)\n            .onAppear {\n                logVisit(person)\n            }\n            .toolbar {\n                ToolbarItemGroup(placement: .secondaryAction) {\n                    SwiftUI.Section {\n                        ActionButtons(person: person)\n                    }\n                }\n            }\n            .popupAnchor()\n            .conditionalNavigationTitle(person.displayName)\n            .navigationBarTitleDisplayMode(.inline)\n            .frame(maxWidth: .infinity, maxHeight: .infinity)\n            .themedGroupedBackground()\n    }\n    \n    @ViewBuilder\n    func content(person: Person) -> some View {\n        FancyScrollView {\n            VStack(spacing: 0) {\n                VStack(spacing: Constants.main.standardSpacing) {\n                    ProfileHeaderView(person, fallback: .personAvatar)\n                    flairsView(person: person)\n                    bio(person: person)\n                }\n                .padding([.horizontal], Constants.main.standardSpacing)\n                \n                VStack(spacing: 0) {\n                    personContent(person: person)\n                }\n            }\n        }\n        .outdatedFeedPopup(feedLoader: feedLoader, showPopup: selectedTab != .communities)\n    }\n    \n    @ViewBuilder\n    func bio(person: Person) -> some View {\n        if let bio = person.description {\n            VStack(spacing: Constants.main.standardSpacing) {\n                let blocks: [BlockNode] = .init(bio)\n                if blocks.isSimpleParagraphs, bio.count < 300 {\n                    MarkdownText(blocks, configuration: .default(palette: palette))\n                        .multilineTextAlignment(.center)\n                    dateLabel(person: person)\n                        .frame(maxWidth: .infinity, alignment: .center)\n                } else {\n                    Markdown(blocks, configuration: .default(palette: palette))\n                    dateLabel(person: person)\n                        .frame(maxWidth: .infinity, alignment: .leading)\n                }\n            }\n            .padding(Constants.main.standardSpacing)\n            .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n            .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        } else {\n            dateLabel(person: person)\n                .frame(maxWidth: .infinity, alignment: .center)\n                .padding(.bottom, Constants.main.halfSpacing)\n        }\n    }\n    \n    @ViewBuilder\n    func flairsView(person: Person) -> some View {\n        if person.isBot || person.isMlemDeveloper || (person.isAdmin.value ?? false) || person.note != nil {\n            HFlow(spacing: Constants.main.halfSpacing) {\n                if person.isMlemDeveloper {\n                    Label(\"Mlem Developer\", systemImage: Icons.developerFlair)\n                        .tint(.themedColorfulAccent(4))\n                }\n                if person.isAdmin.value ?? false {\n                    Label(\"\\(person.host) Administrator\", systemImage: Icons.administrationFill)\n                        .tint(.themedAdministration)\n                }\n                if person.isBot {\n                    Label(\"Bot Account\", icon: .lemmy.botFlair)\n                        .tint(.themedColorfulAccent(5))\n                }\n                if let note = person.note {\n                    Label(note, icon: .lemmy.note)\n                        .tint(.themedNeutralAccent)\n                        .onTapGesture {\n                            navigation.openSheet(.editNote(person))\n                        }\n                }\n                \n            }\n            .labelStyle(FlairLabelStyle())\n        }\n        if person.bannedFromInstance {\n            banFlairView(person: person)\n        }\n    }\n    \n    @ViewBuilder\n    func banFlairView(person: Person) -> some View {\n        HStack {\n            Image(icon: .lemmy.bannedFromInstance)\n                .imageScale(.large)\n                .symbolVariant(.fill)\n            switch person.instanceBan {\n            case let .temporarilyBanned(expires: expires):\n                Text(\"\\(person.name) is banned from \\(person.api.host) until \\(expires.formatted(date: .numeric, time: .omitted)).\")\n            default:\n                Text(\"\\(person.name) is permanently banned from \\(person.api.host).\")\n            }\n        }\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .foregroundStyle(.themedNegative)\n        .padding(Constants.main.standardSpacing)\n        .background(.themedNegative.opacity(0.2), in: .rect(cornerRadius: Constants.main.standardSpacing))\n    }\n    \n    @ViewBuilder\n    func dateLabel(person: Person) -> some View {\n        ProfileDateView(profilable: person)\n            .padding(.horizontal, Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    func personContent(person: Person) -> some View {\n        Section {\n            switch selectedTab {\n            case .communities:\n                communitiesTab(person: person)\n            default:\n                if let feedLoader {\n                    if isProfileTab, selectedTab == .overview || selectedTab == .posts {\n                        Button(\"New Post\", icon: .general.add) {\n                            navigation.openSheet(.createPost(community: nil, type: nil, feedLoader: feedLoader))\n                        }\n                        .buttonStyle(.capsule)\n                        .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n                    }\n                    PersonContentGridView(feedLoader: .singleSourceMixed(feedLoader, contentType: selectedContentType))\n                } else {\n                    ProgressView()\n                }\n            }\n        } header: {\n            BubblePicker(\n                tabs(person: person),\n                selected: $selectedTab,\n                withDividers: [],\n                label: \\.label,\n                value: { tab in\n                    switch tab {\n                    case .posts:\n                        person.postCount.value ?? 0\n                    case .comments:\n                        person.commentCount.value ?? 0\n                    case .communities:\n                        person.moderatedCommunities.value?.count ?? 0\n                    default:\n                        nil\n                    }\n                }\n            )\n        }\n    }\n    \n    @ViewBuilder\n    func communitiesTab(person: Person) -> some View {\n        VStack(spacing: Constants.main.halfSpacing) {\n            ForEach(person.moderatedCommunities.value ?? [], id: \\.actorId) { community in\n                CommunityListRow(community)\n            }\n        }\n        .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n    }\n}\n\nprivate struct FlairLabelStyle: LabelStyle {\n    func makeBody(configuration: Configuration) -> some View {\n        HStack(spacing: 5) {\n            configuration.icon\n                .imageScale(.small)\n            configuration.title\n        }\n        .font(.footnote)\n        .padding(.vertical, 2)\n        .padding(.horizontal, 8)\n        .foregroundStyle(.tint)\n        .background(.tint.opacity(0.2), in: .capsule)\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment(api: .realistic)) {\n//        @Previewable @Environment(AppState.self) var appState\n//        NavigationStack {\n//            PersonView(\n//                appState: appState,\n//                person: .init(Person2.mock(.realistic(.anteSocial45), api: .realistic)),\n//                isProfileTab: true,\n//                visitContext: .other\n//            )\n//        }\n//        .previewTabBar(selected: .profile)\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Pages/UploadConfirmationView.swift",
    "content": "//\n//  UploadConfirmationView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/09/2023.\n//\n\nimport Haptics\nimport MlemMiddleware\nimport PhotosUI\nimport SwiftUI\n\nstruct UploadConfirmationView: View {\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.palette) var palette\n    @Environment(\\.dismiss) var dismiss\n    \n    @Setting(\\.behavior_confirmImageUploads) var confirmImageUploads\n    \n    var imageData: Data\n    var fileExtension: String\n    var imageManager: ImageUploadManager\n    var uploadApi: ApiClient\n    \n    @State var isUploading: Bool = false\n\n    var body: some View {\n        VStack(spacing: 0) {\n            ScrollView {\n                if let uiImage = UIImage(data: imageData) {\n                    Image(uiImage: uiImage)\n                        .resizable()\n                        .aspectRatio(contentMode: .fit)\n                        .clipShape(.rect(cornerRadius: Constants.main.largeItemCornerRadius))\n                }\n                Spacer()\n                    .frame(height: 100)\n            }\n            .scrollIndicators(.hidden)\n            .overlay(alignment: .bottom) {\n                LinearGradient(\n                    colors: [palette.background.primary, Color.clear],\n                    startPoint: .bottom,\n                    endPoint: .top\n                )\n                .frame(height: 100)\n                .allowsHitTesting(false)\n            }\n            VStack(spacing: 0) {\n                VStack(spacing: 16) {\n                    if isUploading {\n                        VStack {\n                            Text(\"Uploading...\")\n                            ProgressView()\n                        }\n                        .font(.title3)\n                        .padding(.vertical, 100)\n                    } else {\n                        Text(\"Upload this image to \\(uploadApi.host)?\")\n                            .font(.largeTitle)\n                            .multilineTextAlignment(.center)\n                        Toggle(\"Ask to confirm every time\", isOn: $confirmImageUploads)\n                            .controlSize(.mini)\n                            .padding(.horizontal)\n                        Button {\n                            Task { @MainActor in\n                                isUploading = true\n                                do {\n                                    try await imageManager.upload(\n                                        data: imageData,\n                                        fileExtension: fileExtension,\n                                        api: uploadApi\n                                    )\n                                    hapticManager.play(haptic: .success, tier: .low)\n                                    dismiss()\n                                } catch {\n                                    handleError(error)\n                                }\n                                isUploading = false\n                            }\n                        } label: {\n                            Text(\"Upload\")\n                                .frame(maxWidth: .infinity)\n                        }\n                        .controlSize(.large)\n                        .fixedSize(horizontal: false, vertical: true)\n                        .buttonStyle(.borderedProminent)\n                        Button {\n                            dismiss()\n                        } label: {\n                            Text(\"Cancel\")\n                                .frame(maxWidth: .infinity)\n                        }\n                        .controlSize(.large)\n                        .buttonStyle(.bordered)\n                    }\n                }\n                .padding(.top, 15)\n                .padding(.bottom, 20)\n                .background(.themedBackground)\n            }\n            .interactiveDismissDisabled()\n            .animation(.easeOut(duration: 0.1), value: isUploading)\n        }\n        .padding()\n        .presentationBackground(.themedBackground)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Pages/VotesListView.swift",
    "content": "//\n//  VotesListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-18.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct VotesListView: View {\n    enum Target: Hashable {\n        case post(Post)\n        case comment(Comment)\n        \n        static func == (lhs: Target, rhs: Target) -> Bool {\n            switch (lhs, rhs) {\n            case let (.post(post1), .post(post2)):\n                post1.actorId == post2.actorId\n            case let (.comment(comment1), .comment(comment2)):\n                comment1.actorId == comment2.actorId\n            default:\n                false\n            }\n        }\n        \n        func hash(into hasher: inout Hasher) {\n            switch self {\n            case let .post(post): hasher.combine(post.actorId)\n            case let .comment(comment): hasher.combine(comment.actorId)\n            }\n        }\n        \n        var model: any InteractableProviding {\n            switch self {\n            case let .post(post): post\n            case let .comment(comment): comment\n            }\n        }\n    }\n    \n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    let target: Target\n    \n    @State var votes: [PersonVote] = []\n    @State var page: Int = 1\n    @State var loadingState: LoadingState = .idle\n    \n    var body: some View {\n        FancyScrollView {\n            LazyVStack(spacing: Constants.main.halfSpacing) {\n                ForEach(votes, id: \\.creator.id, content: rowView)\n                EndOfFeedView(loadingState: loadingState, viewType: .turtle)\n                    .onAppear {\n                        loadNextPage()\n                    }\n            }\n        }\n        .environment(\\.communityContext, target.model.community.value)\n        .themedGroupedBackground()\n        .navigationTitle(\"Votes\")\n        .navigationBarTitleDisplayMode(.inline)\n    }\n    \n    @ViewBuilder\n    func rowView(_ vote: PersonVote) -> some View {\n        NavigationLink(.person(vote.creator)) {\n            rowViewLabel(vote)\n        }\n        .padding([.vertical, .trailing], Constants.main.halfSpacing)\n        .padding(.leading, Constants.main.standardSpacing)\n        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n        .contextMenu(person: vote.creator)\n        .padding(.horizontal, Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    func rowViewLabel(_ vote: PersonVote) -> some View {\n        HStack {\n            FullyQualifiedLinkView(vote.creator, labelStyle: .medium)\n            Spacer()\n            Image(systemName: vote.vote.systemImage)\n                .imageScale(.large)\n                .symbolVariant(.fill)\n                .symbolRenderingMode(.palette)\n                .foregroundStyle(.themedContrastingLabel, vote.vote.color)\n        }\n    }\n    \n    func loadNextPage() {\n        Task { @MainActor in\n            guard loadingState == .idle else { return }\n            loadingState = .loading\n            do {\n                let newVotes: [PersonVote]\n                switch target {\n                case let .post(post):\n                    newVotes = try await post.getVotes(page: page, limit: 40)\n                case let .comment(comment):\n                    // TODO: handle this better--call refresh first?\n                    guard let communityId = comment.community.value_?.id else {\n                        assertionFailure(\"loadNextPage called without resolved community\")\n                        newVotes = .init()\n                        break\n                    }\n                    newVotes = try await comment.getVotes(page: page, limit: 40, communityId: communityId)\n                }\n                votes.append(contentsOf: newVotes)\n                if newVotes.count < 40 {\n                    loadingState = .done\n                } else {\n                    loadingState = .idle\n                }\n                page += 1\n            } catch {\n                handleError(error)\n                loadingState = .idle\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/AppDelegate.swift",
    "content": "//\n//  AppDelegate.swift\n//  Mlem\n//\n//  Created by tht7 on 02/07/2023.\n//\n\nimport Foundation\nimport SwiftUI\n\n// TODO: we need to do a bit of work to ensure we also switch tab when responding to these\n// as currently it launches you into the app, but if the app was already running you're left\n// on the tab/screen you were on - despite the shortcuts being designed to take you to the \"Feeds\" tab\nvar shortcutItemToProcess: UIApplicationShortcutItem?\n\nclass AppDelegate: UIResponder, UIApplicationDelegate, UIWindowSceneDelegate {\n    func application(\n        _ application: UIApplication,\n        configurationForConnecting connectingSceneSession: UISceneSession,\n        options: UIScene.ConnectionOptions\n    )\n        -> UISceneConfiguration {\n        if let shortcutItem = options.shortcutItem {\n            shortcutItemToProcess = shortcutItem\n        }\n\n        let sceneConfiguration = UISceneConfiguration(name: \"Custom Configuration\", sessionRole: connectingSceneSession.role)\n        sceneConfiguration.delegateClass = CustomSceneDelegate.self\n\n        return sceneConfiguration\n    }\n}\n\nclass CustomSceneDelegate: UIResponder, UIWindowSceneDelegate {\n    func windowScene(\n        _ windowScene: UIWindowScene,\n        performActionFor shortcutItem: UIApplicationShortcutItem,\n        completionHandler: @escaping (Bool) -> Void\n    ) {\n        shortcutItemToProcess = shortcutItem\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/ContentView+Logic.swift",
    "content": "//\n//  ContentView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2024.\n//\n\nimport Haptics\nimport MlemMiddleware\nimport Nuke\nimport Rest\nimport SwiftUI\n\nextension ContentView {\n    var shouldDisplayToasts: Bool {\n        navigationModel.layers.allSatisfy { !$0.canDisplayToasts }\n    }\n    \n    var avatarRefreshHash: Int {\n        var hasher = Hasher()\n        hasher.combine(appState.firstAccount.avatar)\n        hasher.combine(tabProfileShowAvatar)\n        hasher.combine(colorPalette)\n        hasher.combine(colorScheme)\n        return hasher.finalize()\n    }\n    \n    func loadAvatar(url: URL) async {\n        do {\n            if tabProfileShowAvatar {\n                let urlRequest = mlemUrlRequest(url: url.withIconSize(128))\n                let imageTask = ImagePipeline.shared.imageTask(with: .init(urlRequest: urlRequest))\n                let avatarImage = try await imageTask.image\n                    .resized(to: .init(width: imageTask.image.size.width / imageTask.image.size.height * 26, height: 26))\n                    .circleMasked\n                    .withRenderingMode(.alwaysOriginal)\n                \n                let selectedAvatarImage = try await imageTask.image\n                    .resized(to: .init(width: imageTask.image.size.width / imageTask.image.size.height * 26, height: 26))\n                    .circleBorder(color: .init(colorPalette.palette.accent), width: 3.5)\n                    .withRenderingMode(.alwaysOriginal)\n                \n                Task { @MainActor in\n                    self.avatarImage = avatarImage\n                    self.selectedAvatarImage = selectedAvatarImage\n                }\n            }\n        } catch {\n            handleError(error, silent: true)\n        }\n    }\n    \n    func handleHapticError(_ error: HapticError) {\n        handleError(error, silent: !developerMode)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/ContentView+Tab.swift",
    "content": "//\n//  ContentView+Tab.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-23.\n//\n\nimport Foundation\nimport Icons\nimport SwiftUI\n\nextension ContentView {\n    enum Tab: CaseIterable {\n        case feeds, inbox, profile, search, settings\n        \n        var defaultLabel: LocalizedStringResource {\n            switch self {\n            case .feeds: \"Feeds\"\n            case .inbox: \"Inbox\"\n            case .profile: \"Profile\"\n            case .search: \"Search\"\n            case .settings: \"Settings\"\n            }\n        }\n        \n        func label(appState: AppState, profileLabelType: ProfileTabLabel) -> String {\n            switch self {\n            case .profile:\n                switch profileLabelType {\n                case .nickname:\n                    appState.firstAccount.nickname\n                case .instance:\n                    appState.firstAccount.host\n                case .anonymous:\n                    .init(localized: \"Profile\")\n                }\n            default:\n                .init(localized: defaultLabel)\n            }\n        }\n        \n        var icon: Icon {\n            switch self {\n            case .feeds: .lemmy.feed\n            case .inbox: .lemmy.inbox\n            case .profile: .lemmy.personAvatar\n            case .search: .general.search\n            case .settings: .general.settings\n            }\n        }\n    }\n}\n\nextension CustomTabItem {\n    init(\n        _ tab: ContentView.Tab,\n        appState: AppState,\n        profileLabelType: ProfileTabLabel,\n        imageOverride: UIImage? = nil,\n        selectedImageOverride: UIImage? = nil,\n        badge: String? = nil,\n        onLongPress: (() -> Void)? = nil,\n        @ViewBuilder content: () -> some View\n    ) {\n        self.init(\n            title: tab.label(appState: appState, profileLabelType: profileLabelType),\n            image: imageOverride ?? .init(icon: tab.icon.representingState(active: false)),\n            selectedImage: selectedImageOverride ?? .init(icon: tab.icon.representingState(active: true)),\n            badge: badge,\n            onLongPress: onLongPress, content: content\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/ContentView.swift",
    "content": "//\n//  ContentView.swift\n//  Mlem\n//\n//  Created by David Bureš on 25.03.2022.\n//\n\nimport Dependencies\nimport Haptics\nimport MlemBackend\nimport MlemMiddleware\nimport Nuke\nimport QuickSwipes\nimport SwiftUI\nimport Theming\n\nstruct ContentView: View {\n    @Environment(\\.scenePhase) var scenePhase\n    @Environment(\\.colorScheme) var colorScheme\n    \n    @Dependency(\\.persistenceRepository) var persistenceRepository\n    \n    @Setting(\\.appearance_palette) var colorPalette\n    @Setting(\\.tab_profile_labelType) var tabProfileLabelType\n    @Setting(\\.tab_profile_showAvatar) var tabProfileShowAvatar\n    @Setting(\\.tab_gestures_longPressAction) var tabLongPressAction\n    @Setting(\\.dev_developerMode) var developerMode\n    @Setting(\\.behavior_hapticLevel) var hapticLevel\n    @Setting(\\.behavior_enableQuickSwipes) var quickSwipesEnabled\n\n    let cacheCleanTimer = Timer.publish(every: 10, on: .main, in: .common).autoconnect()\n    let unreadCountTimer = Timer.publish(every: 30, on: .main, in: .common).autoconnect()\n    \n    // globals\n    var appState: AppState { .main }\n    var tabReselectTracker: TabReselectTracker { .main }\n    var navigationModel: NavigationModel { .main }\n\n    var filtersTracker: FiltersTracker { .main }\n    var errorsTracker: ErrorsTracker { .main }\n    var backendClient: BackendClient { .main }\n    \n    @State var avatarImage: UIImage?\n    @State var selectedAvatarImage: UIImage?\n    \n    @State var expandedPostHistoryTracker: ExpandedPostHistoryTracker = .init()\n    @State var eventsTracker: EventsTracker = .init()\n    \n    var body: some View {\n        if appState.appRefreshToggle {\n            content\n                .task(id: avatarRefreshHash) {\n                    avatarImage = nil\n                    selectedAvatarImage = nil\n                    if let url = appState.firstAccount.avatar {\n                        await loadAvatar(url: url)\n                    }\n                }\n                .onReceive(cacheCleanTimer) { _ in\n                    appState.cleanCaches()\n                }\n                .onReceive(unreadCountTimer) { _ in\n                    Task { @MainActor in\n                        try await (appState.firstSession as? UserSession)?.unreadCount?.refresh()\n                    }\n                }\n                .navigationSheetModifiers(\n                    nextLayer: navigationModel.layers.first,\n                    isTopSheet: navigationModel.layers.isEmpty,\n                    shareInfo: .init(get: { navigationModel.shareInfo }, set: { navigationModel.shareInfo = $0 }),\n                    contentPickerTracker: navigationModel.contentPickerTracker\n                )\n                .accentColor(ThemedColor.themedAccent.resolve(with: colorPalette.palette)) // deprecated, but .tint colors menu buttons\n                .palette(colorPalette.palette)\n                .environment(tabReselectTracker)\n                .environment(appState)\n                .environment(filtersTracker)\n                .environment(errorsTracker)\n                .environment(expandedPostHistoryTracker)\n                .environment(backendClient)\n                .environment(eventsTracker)\n                .environment(ToastModel.main)\n                .quickSwipesDisabled(!quickSwipesEnabled)\n                .quickSwipeThresholds(primary: 60, secondary: 150, tertiary: 240)\n                .quickSwipeMinimumDrag(20)\n                .quickSwipeCornerRadius(Constants.main.standardSpacing)\n                .quickSwipeIconSize(28)\n                .task(id: BackendClient.main.environment) {\n                    await MlemStats.main.loadInstances(forceRefresh: true)\n                }\n                .onChange(of: appState.firstPerson) {\n                    // Observe AppState.main.firstPerson to update FiltersTracker as needed\n                    // TODO: when Observation adds continous observation monitoring, move this into FiltersTracker\n                    filtersTracker.moderatedCommunityActorIds = appState.firstPerson?.moderatedCommunityActorIds ?? .init()\n                }\n                .onChange(of: scenePhase, initial: false) {\n                    if AppState.main.firstAccount is UserAccount, scenePhase != .active {\n                        Task {\n                            do {\n                                try await AppState.main.firstApi.flushPostReadQueue()\n                            } catch {\n                                handleError(error)\n                            }\n                        }\n                    }\n                }\n                .onChange(of: scenePhase) {\n                    if scenePhase == .active {\n                        eventsTracker.refreshIfStale()\n                    }\n                }\n                .hapticConfiguration(maximumHapticTier: hapticLevel, errorHandler: handleHapticError)\n                .environment(AppState.main)\n                .onOpenURL { url in\n                    guard url.scheme == \"mlem\" else { return }\n                    var components = URLComponents(url: url, resolvingAgainstBaseURL: false)\n                    components?.scheme = \"https\"\n                    guard let targetURL = components?.url else { return }\n                    navigationModel.pendingOpenURL = targetURL\n                }\n        }\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        CustomTabView(selectedIndex: Binding(get: {\n            Tab.allCases.firstIndex(of: appState.contentViewTab) ?? 0\n        }, set: {\n            appState.contentViewTab = Tab.allCases[$0]\n        }), tabs: [\n            CustomTabItem(.feeds, appState: appState, profileLabelType: tabProfileLabelType) {\n                NavigationSplitRootView(sidebar: .subscriptionList, root: .feeds())\n            },\n            CustomTabItem(\n                .inbox,\n                appState: appState,\n                profileLabelType: tabProfileLabelType,\n                badge: (appState.firstSession as? UserSession)?.unreadCount?.badgeLabel.map { String($0) }\n            ) {\n                NavigationLayerView(layer: .init(root: .inbox, model: navigationModel), hasSheetModifiers: false)\n            },\n            CustomTabItem(\n                .profile,\n                appState: appState,\n                profileLabelType: tabProfileLabelType,\n                imageOverride: avatarImage ?? UIImage(systemName: \"person.crop.circle\"),\n                selectedImageOverride: selectedAvatarImage ?? UIImage(systemName: \"person.crop.circle.fill\"),\n                onLongPress: {\n                    HapticManager.main.play(haptic: .rigidInfo, tier: .high)\n                    \n                    switch tabLongPressAction {\n                    case .openAccountSwitcher:\n                        navigationModel.openSheet(.quickSwitcher)\n                    case .switchToMostRecentAccount:\n                        // If switch fails (no other accounts), fall back to account switcher.\n                        if !appState.switchToMostRecentAccount() {\n                            navigationModel.openSheet(.quickSwitcher)\n                        }\n                    }\n                },\n                content: {\n                    NavigationLayerView(layer: .init(root: .profile, model: navigationModel), hasSheetModifiers: false)\n                }\n            ),\n            CustomTabItem(.search, appState: appState, profileLabelType: tabProfileLabelType) {\n                NavigationLayerView(layer: .init(root: .search, model: navigationModel), hasSheetModifiers: false)\n            },\n            CustomTabItem(.settings, appState: appState, profileLabelType: tabProfileLabelType) {\n                NavigationLayerView(layer: .init(root: .settings(), model: navigationModel), hasSheetModifiers: false)\n            }\n        ], onSwipeUp: {\n            navigationModel.openSheet(.quickSwitcher)\n        })\n        .withAccountSwitcherGesture(tabReselectTracker: tabReselectTracker, navigationModel: navigationModel)\n        .overlay(alignment: .bottom) {\n            ToastOverlayView(\n                shouldDisplayNewToasts: shouldDisplayToasts,\n                location: .bottom\n            )\n            .padding(.bottom, 100)\n        }\n        .ignoresSafeArea()\n        .overlay(alignment: .top) {\n            ToastOverlayView(\n                shouldDisplayNewToasts: shouldDisplayToasts,\n                location: .top\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/LoginCredentialsView.swift",
    "content": "//\n//  LoginCredentialsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/05/2024.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct LoginCredentialsView: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(AppState.self) var appState\n    @Environment(\\.dismiss) var dismiss\n    @Environment(\\.isRootView) var isRootView\n\n    @State var instance: Instance?\n    let account: UserAccount?\n    \n    @State var upgradeState: LoadingState = .idle\n    \n    @State var usernameOrEmail: String\n    @State var password: String = \"\"\n    \n    @State var authenticating: Bool = false\n    @State private var failureReason: FailureReason?\n    \n    enum FocusedField { case usernameOrEmail, password }\n    @FocusState private var focused: FocusedField?\n    \n    var showUsernameField: Bool { account == nil }\n    \n    init(instance: Instance) {\n        self.instance = instance\n        self.account = nil\n        self._usernameOrEmail = .init(wrappedValue: \"\")\n    }\n    \n    init(account: UserAccount) {\n        self.instance = nil\n        self.account = account\n        self._usernameOrEmail = .init(wrappedValue: account.name)\n    }\n    \n    var body: some View {\n        content\n            .frame(maxWidth: .infinity)\n            .themedGroupedBackground()\n            .interactiveDismissDisabled((!usernameOrEmail.isEmpty && showUsernameField) || !password.isEmpty)\n            .toolbar {\n                if navigation.isInsideSheet, isRootView {\n                    ToolbarItem(placement: .topBarLeading) {\n                        CloseButtonView(ios18Label: .cancel)\n                            .disabled(authenticating)\n                    }\n                }\n            }\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        ScrollView {\n            VStack {\n                if let instance {\n                    instanceHeader(instance)\n                } else if let account {\n                    reauthHeader(account)\n                        .padding(.bottom, 15)\n                }\n                textFields\n                nextButton\n                    .padding(.top, 5)\n                if let failureReason {\n                    Text(failureReason.label)\n                        .foregroundStyle(.red)\n                }\n            }\n            .frame(maxWidth: .infinity)\n            .padding(.horizontal)\n        }\n        .scrollBounceBehavior(.basedOnSize)\n    }\n    \n    @ViewBuilder\n    func instanceHeader(_ instance: Instance) -> some View {\n        CircleCroppedImageView(url: instance.avatar, frame: 50, fallback: .instanceAvatar)\n        Text(instance.displayName)\n            .font(.title)\n            .bold()\n    }\n    \n    @ViewBuilder\n    func reauthHeader(_ account: UserAccount) -> some View {\n        VStack {\n            CircleCroppedImageView(account, frame: 50)\n            Text(account.fullName ?? \"Sign In\")\n                .font(.title)\n                .bold()\n                .padding(.bottom, 5)\n            Text(\"Your session has expired. Enter your password to authenticate a new session.\")\n                .foregroundStyle(.secondary)\n                .multilineTextAlignment(.center)\n        }\n    }\n    \n    @ViewBuilder\n    var textFields: some View {\n        Grid(\n            alignment: .trailing,\n            horizontalSpacing: 15,\n            verticalSpacing: 0\n        ) {\n            if showUsernameField {\n                GridRow {\n                    Text(\"Username\")\n                        .padding([.leading, .vertical])\n                    TextField(\"Username\", text: $usernameOrEmail, prompt: Text(verbatim: \"\"))\n                        .focused($focused, equals: .usernameOrEmail)\n                        .onSubmit { focused = .password }\n                        .padding(.trailing)\n                }\n                Divider()\n            }\n            GridRow {\n                Text(\"Password\")\n                    .padding([.leading, .vertical])\n                SecureField(\"Password\", text: $password, prompt: Text(verbatim: \"\"))\n                    .focused($focused, equals: .password)\n                    .padding(.trailing)\n                    .onSubmit(attemptToLogin)\n                    .submitLabel(.go)\n            }\n        }\n        .textInputAutocapitalization(.never)\n        .autocorrectionDisabled()\n        .background(\n            RoundedRectangle(cornerRadius: 16)\n                .fill(.themedSecondaryGroupedBackground)\n        )\n        .paletteBorder(cornerRadius: 16)\n        .onAppear { focused = showUsernameField ? .usernameOrEmail : .password }\n        .onChange(of: usernameOrEmail) { failureReason = nil }\n        .onChange(of: password) { failureReason = nil }\n    }\n    \n    @ViewBuilder\n    var nextButton: some View {\n        Button(action: attemptToLogin) {\n            Text(authenticating ? \"Authenticating...\" : \"Sign In\")\n                .padding(.vertical, 10)\n                .frame(maxWidth: .infinity)\n                .transaction { $0.animation = .none }\n        }\n        .buttonStyle(.borderedProminent)\n        .buttonBorderShape(.roundedRectangle(radius: 16))\n        .disabled(usernameOrEmail.isEmpty || password.isEmpty || authenticating)\n    }\n    \n    func attemptToLogin() {\n        guard !usernameOrEmail.isEmpty, !password.isEmpty else { return }\n        if let client = instance?.guestApi ?? account?.api.asGuest() {\n            authenticating = true\n            Task {\n                do {\n                    let user = try await AccountsTracker.main.logIn(\n                        client: client,\n                        usernameOrEmail: usernameOrEmail,\n                        password: password\n                    )\n                    appState.changeAccount(to: user)\n                    if navigation.isTopSheet {\n                        navigation.dismissSheet()\n                    }\n                } catch {\n                    switch error {\n                    case let ApiClientError.response(response, _) where response.error == \"missing_totp_token\":\n                        navigation.push(.logIn(.totp(client: client, usernameOrEmail: usernameOrEmail, password: password)))\n                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                            authenticating = false\n                        }\n                    case ApiClientError.invalidSession:\n                        failureReason = .incorrectPassword\n                    default:\n                        handleError(error, silent: true)\n                        failureReason = .other\n                    }\n                    Task { @MainActor in\n                        authenticating = false\n                    }\n                }\n            }\n        }\n    }\n}\n\nprivate enum FailureReason {\n    case incorrectPassword\n    case other\n    \n    var label: String {\n        switch self {\n        case .incorrectPassword:\n            \"Username or password is incorrect.\"\n        case .other:\n            \"Something went wrong.\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/LoginInstancePickerView.swift",
    "content": "//\n//  LoginInstancePickerView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 10/05/2024.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct LoginInstancePickerView: View {\n    @Environment(\\.palette) var palette\n    @Environment(\\.dismiss) var dismiss\n    @Environment(\\.isRootView) var isRootView\n    @Environment(NavigationLayer.self) var navigation\n    \n    @State var domain: String = \"\"\n    \n    @State var connecting: Bool = false\n    @State var invalidInstance: Bool = false\n    @State private var scrollViewContentSize: CGSize = .zero\n    @FocusState private var focused: Bool\n    \n    var body: some View {\n        content\n            .interactiveDismissDisabled(!domain.isEmpty)\n            .background(.themedGroupedBackground)\n            .presentationBackground(.themedGroupedBackground)\n            .toolbar {\n                if navigation.isInsideSheet, isRootView {\n                    ToolbarItem(placement: .topBarLeading) {\n                        CloseButtonView(ios18Label: .cancel)\n                            .disabled(connecting)\n                    }\n                }\n            }\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        let filteredSuggestions = MlemStats.main.instances?.lazy.map(\\.host).filter {\n            $0.starts(with: domain) && $0 != domain\n        } ?? []\n        let showSuggestions = !(filteredSuggestions.isEmpty || domain.isEmpty || !focused)\n        VStack {\n            Image(systemName: \"globe\")\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .frame(height: 50)\n                .foregroundStyle(.themedAccent)\n            Text(\"Sign In to Lemmy\")\n                .font(.title)\n                .bold()\n            Text(\"Enter your instance's domain name below.\")\n                .foregroundStyle(.themedSecondary)\n                .multilineTextAlignment(.center)\n                .padding(.bottom, 5)\n            instanceSuggestionsBox(suggestions: filteredSuggestions)\n                .padding(showSuggestions ? .vertical : .top)\n            if !showSuggestions {\n                nextButton\n                    .padding(.top, 5)\n            }\n            if invalidInstance {\n                Text(\"Failed to connect to \\(domain)\")\n                    .foregroundStyle(.themedNegative)\n                    .multilineTextAlignment(.center)\n            }\n            Spacer()\n        }\n        .padding(.horizontal)\n    }\n    \n    @ViewBuilder\n    func instanceSuggestionsBox(suggestions: [String]) -> some View {\n        VStack(spacing: 0) {\n            instanceField\n            if !suggestions.isEmpty, !domain.isEmpty, focused {\n                ScrollView {\n                    VStack(alignment: .leading, spacing: 0) {\n                        ForEach(suggestions, id: \\.self) { text in\n                            Divider()\n                            Button {\n                                domain = text\n                                focused = false\n                                attemptToConnect()\n                            } label: {\n                                Text(attributedString(suggestion: text))\n                                    .frame(maxWidth: .infinity, alignment: .leading)\n                            }\n                            .padding()\n                        }\n                    }\n                    .background(\n                        GeometryReader { geo in\n                            geometryReaderBackground(geoSize: geo.size)\n                        }\n                    )\n                }\n                .frame(maxHeight: scrollViewContentSize.height)\n                .scrollBounceBehavior(.basedOnSize)\n            }\n        }\n        .frame(maxWidth: .infinity)\n        .background(.themedSecondaryGroupedBackground)\n        .clipShape(RoundedRectangle(cornerRadius: 16))\n        .paletteBorder(cornerRadius: 16)\n    }\n    \n    @ViewBuilder\n    var instanceField: some View {\n        TextField(\n            \"Domain\",\n            text: $domain,\n            prompt: Text(\"example.com\")\n        )\n        .disabled(connecting)\n        .focused($focused)\n        .keyboardType(.URL)\n        .textInputAutocapitalization(.never)\n        .autocorrectionDisabled()\n        .scrollDismissesKeyboard(.never)\n        .padding()\n        .submitLabel(.go)\n        .onSubmit {\n            if !domain.isEmpty {\n                attemptToConnect()\n            }\n        }\n        .onTapGesture {\n            if !connecting { focused = true }\n        }\n        .onAppear { focused = true }\n        .onChange(of: domain) {\n            invalidInstance = false\n        }\n    }\n    \n    @ViewBuilder\n    var nextButton: some View {\n        Button(action: attemptToConnect) {\n            Text(connecting ? \"Connecting...\" : \"Next\")\n                .padding(.vertical, 10)\n                .frame(maxWidth: .infinity)\n                .transaction { $0.animation = .none }\n        }\n        .buttonStyle(.borderedProminent)\n        .buttonBorderShape(.roundedRectangle(radius: 16))\n        .transaction { $0.animation = .none }\n        .disabled(!(domain.contains(/.+\\..+$/) || domain.starts(with: \"localhost:\")) || connecting)\n    }\n    \n    func geometryReaderBackground(geoSize: CGSize) -> some View {\n        Task { @MainActor in\n            scrollViewContentSize = geoSize\n        }\n        return Color.clear\n    }\n    \n    func attemptToConnect() {\n        guard !connecting else { return }\n        var domain = domain\n        if !domain.contains(\"://\") {\n            domain = domain.starts(with: \"localhost:\") ? \"http://\\(domain)\" : \"https://\\(domain)\"\n        }\n        if let url = URL(string: domain) {\n            focused = false\n            connecting = true\n            let fetchTask = Task {\n                let apiClient = ApiClient.getApiClient(url: url, username: nil)\n                do {\n                    let instance = try await apiClient.getMyInstance()\n                    Task { @MainActor in\n                        navigation.push(.logIn(.instance(instance)))\n                    }\n                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                        connecting = false\n                    }\n                } catch {\n                    handleError(error, silent: true)\n                    Task { @MainActor in\n                        connecting = false\n                        invalidInstance = true\n                    }\n                }\n            }\n            DispatchQueue.main.asyncAfter(deadline: .now() + 5) {\n                fetchTask.cancel()\n                invalidInstance = true\n            }\n        }\n    }\n    \n    func attributedString(suggestion string: String) -> AttributedString {\n        var attributedString = AttributedString(stringLiteral: string)\n        attributedString.foregroundColor = .secondary\n        if string.starts(with: domain) {\n            let range = ..<attributedString.index(attributedString.startIndex, offsetByCharacters: domain.count)\n            attributedString[range].foregroundColor = .primary\n        }\n        return attributedString\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/LoginTotpView.swift",
    "content": "//\n//  LoginTotpView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 13/05/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct LoginTotpView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    let client: ApiClient\n    let usernameOrEmail: String\n    let password: String\n    \n    @State var totpToken: String = \"\"\n    @State var authenticating: Bool = false\n    @State var incorrect: Bool = false\n    \n    @FocusState private var focused: Bool\n    \n    let fontSize: CGFloat = 40\n    let characterSpacing: CGFloat = 30\n    let characterPadding: CGFloat = 8\n    \n    var body: some View {\n        VStack {\n            Image(systemName: \"person.badge.key.fill\")\n                .symbolRenderingMode(.hierarchical)\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .frame(height: 50)\n                .foregroundStyle(.themedAccent)\n            Text(\"Two-Factor Authentication\")\n                .font(.title)\n                .bold()\n                .multilineTextAlignment(.center)\n            codeInput\n                .padding(.bottom, 5)\n            if incorrect, totpToken.count == 0 {\n                Text(\"Authentication code is incorrect.\")\n                    .foregroundStyle(.themedNegative)\n            }\n            openInAppButton\n            if authenticating {\n                ProgressView()\n                    .controlSize(.large)\n                    .tint(.themedSecondary)\n                    .padding(.top)\n            }\n            Spacer()\n        }\n        .frame(maxWidth: .infinity, maxHeight: .infinity)\n        .padding(.horizontal)\n        .themedGroupedBackground()\n    }\n    \n    @ViewBuilder\n    var codeInput: some View {\n        ZStack {\n            TextField(String(\"\"), text: Binding(\n                get: { totpToken },\n                set: { newValue in\n                    let trimmedValue = String(newValue.prefix(6))\n                    totpToken = trimmedValue\n                    if trimmedValue.count == 6, !authenticating {\n                        focused = false\n                        attemptToLogin()\n                    }\n                }\n            ))\n            .kerning(characterSpacing)\n            .font(.system(size: fontSize))\n            .monospaced()\n            .focused($focused)\n            .offset(x: characterPadding * 2)\n            .keyboardType(.numberPad)\n            .textContentType(.oneTimeCode)\n            .disabled(authenticating)\n        }\n        .frame(width: fontSize * 0.615 * 6 + characterSpacing * 5 + characterPadding * 4)\n        .padding(.vertical, 5)\n        .background {\n            HStack(spacing: characterSpacing - characterPadding * 2) {\n                ForEach(0 ..< 6, id: \\.self) { _ in\n                    RoundedRectangle(cornerRadius: 8)\n                        .fill(Color(uiColor: .secondarySystemGroupedBackground))\n                }\n            }\n            .padding(.horizontal, characterPadding)\n        }\n        .onAppear { focused = true }\n    }\n    \n    @ViewBuilder\n    var openInAppButton: some View {\n        let authAppUrl = URL(string: \"totp://\")!\n        Button(\"Open Authenticator App...\") {\n            UIApplication.shared.open(authAppUrl)\n        }\n    }\n    \n    func attemptToLogin() {\n        authenticating = true\n        Task {\n            do {\n                let user = try await AccountsTracker.main.logIn(\n                    client: client,\n                    usernameOrEmail: usernameOrEmail,\n                    password: password,\n                    totpToken: totpToken\n                )\n                appState.changeAccount(to: user)\n                if navigation.isTopSheet {\n                    navigation.dismissSheet()\n                }\n            } catch {\n                handleError(error, silent: true)\n                Task { @MainActor in\n                    authenticating = false\n                    totpToken = \"\"\n                    focused = true\n                    incorrect = true\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/Onboarding/OnboardingEmailView.swift",
    "content": "//\n//  OnboardingEmailView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-05-30.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct OnboardingEmailView: View {\n    @Environment(OnboardingModel.self) var model\n    \n    @State var email: String = \"\"\n    @FocusState var focused: Bool\n    \n    var body: some View {\n        VStack {\n            Text(\"Email Address\")\n                .font(.title)\n                .fontWeight(.bold)\n            Text(\"You cannot use a temporary email address.\")\n                .multilineTextAlignment(.center)\n                .foregroundStyle(.secondary)\n            VStack(spacing: 16) {\n                textFieldView\n                nextButtonView\n            }\n        }\n        .padding(.horizontal, 16)\n        .padding(.bottom, 30)\n        .frame(minHeight: 0, maxHeight: .infinity) // Min height is needed here otherwise the keyboard padding doesn't work properly\n        .keyboardAwarePadding(removePaddingOnDismiss: false)\n        .overlay(alignment: .topLeading) {\n            Button(\"Back\", icon: .general.backward) {\n                model.page = .username\n            }\n            .fontWeight(.semibold)\n            .imageScale(.large)\n            .labelStyle(.iconOnly)\n            .padding()\n        }\n        .frame(maxHeight: .infinity)\n    }\n    \n    @ViewBuilder\n    var textFieldView: some View {\n        TextField(\"Email\", text: $email, prompt: Text(verbatim: \"\"))\n            .autocorrectionDisabled()\n            .textInputAutocapitalization(.never)\n            .submitLabel(.done)\n            .onSubmit {}\n            .focused($focused)\n            .onAppear {\n                focused = true\n            }\n            .padding()\n            .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: 16))\n    }\n    \n    @ViewBuilder\n    var nextButtonView: some View {\n        Button(action: submit) {\n            Text(\"Next\")\n                .frame(maxWidth: .infinity)\n                .padding(.vertical, 10)\n        }\n        .buttonStyle(.borderedProminent)\n        .buttonBorderShape(.roundedRectangle(radius: 16))\n        .disabled(!emailIsValid)\n    }\n    \n    var emailIsValid: Bool {\n        do {\n            let regex = /[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}/\n            return try (regex.wholeMatch(in: email) != nil)\n        } catch {\n            assertionFailure(String(describing: error))\n            return false\n        }\n    }\n    \n    func submit() {\n        model.email = email\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/Onboarding/OnboardingModel.swift",
    "content": "//\n//  OnboardingModel.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-05-30.\n//\n\nimport Foundation\nimport MlemMiddleware\n\n@Observable\nclass OnboardingModel {\n    enum Page { case recommendInstance, username, email }\n    \n    var page: Page = .recommendInstance\n    \n    var instance: Instance?\n    var username: String?\n    var email: String?\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/Onboarding/OnboardingRecommendInstanceView.swift",
    "content": "//\n//  OnboardingRecommendInstanceView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-05-27.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct OnboardingRecommendInstanceView: View {\n    @Environment(\\.colorScheme) var colorScheme\n    let instance: Instance?\n    let submit: () -> Void\n    \n    @State var showButtons: Bool = false\n    \n    private let lightModeForeground: Color = .init(red: 40 / 255, green: 113 / 255, blue: 127 / 255)\n    \n    var body: some View {\n        VStack {\n            Spacer()\n            if let instance {\n                text(instance)\n                    .transition(.scale.combined(with: .opacity))\n                    .onAppear {\n                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                            showButtons = true\n                        }\n                    }\n                buttons\n                    .opacity(showButtons ? 1 : 0)\n                    .scaleEffect(showButtons ? 1 : 0.9)\n                    .animation(.bouncy, value: showButtons)\n            }\n            Spacer()\n            Spacer()\n        }\n        .frame(maxWidth: .infinity)\n        .frame(maxHeight: .infinity)\n        .overlay(alignment: .topLeading) {\n            CloseButtonView()\n                .padding()\n        }\n        .onAppear {\n            if instance != nil {\n                showButtons = true\n            }\n        }\n    }\n    \n    func text(_ instance: Instance) -> some View {\n        ExpectedView(instance.activeUserCount) { activeUserCount in\n            Text(\"Join \\(numberText(activeUserCount.month)) active users on Lemmy.world\")\n                .foregroundStyle(colorScheme == .dark ? .white : .black)\n                .compositingGroup()\n                .font(.largeTitle)\n                .fontWeight(.bold)\n                .multilineTextAlignment(.center)\n                .padding(.horizontal, 20)\n        }\n    }\n    \n    var buttons: some View {\n        VStack {\n            Button(action: submit) {\n                Text(\"Let's Go\")\n                    .padding(.vertical, 10)\n                    .padding(.horizontal, 50)\n            }\n            .buttonStyle(.borderedProminent)\n            .buttonBorderShape(.roundedRectangle(radius: 16))\n            .tint(colorScheme == .dark ? .blue : lightModeForeground)\n            Button {} label: {\n                Text(\"Choose another instance...\")\n                    .foregroundStyle(.gray)\n                    .opacity(0.5)\n            }\n            .buttonStyle(.empty)\n            .padding(.top, 5)\n        }\n    }\n    \n    func numberText(_ value: Int) -> Text {\n        if colorScheme == .dark {\n            Text(String(value))\n                .foregroundStyle(.teal.gradient.shadow(.drop(color: .blue, radius: 10)))\n        } else {\n            Text(String(value))\n                .foregroundStyle(lightModeForeground)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/Onboarding/OnboardingUsernameView.swift",
    "content": "//\n//  OnboardingUsernameView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-05-24.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct OnboardingUsernameView: View {\n    @Environment(OnboardingModel.self) var model\n    \n    @State var username: String = \"\"\n    @FocusState var focused: Bool\n    \n    @State var usernameValidity: UsernameValidity?\n    \n    var body: some View {\n        VStack {\n            Text(\"Choose a Username\")\n                .font(.title)\n                .fontWeight(.bold)\n            Text(\"This cannot be changed later.\")\n                .multilineTextAlignment(.center)\n                .foregroundStyle(.secondary)\n            VStack(spacing: 16) {\n                textFieldView\n                nextButtonView\n            }\n            validityWarningView\n        }\n        .padding(.horizontal, 16)\n        .frame(minHeight: 0, maxHeight: .infinity) // Min height is needed here otherwise the keyboard padding doesn't work properly\n        .keyboardAwarePadding(removePaddingOnDismiss: false)\n        .overlay(alignment: .topLeading) {\n            Button(\"Back\", icon: .general.backward) {\n                focused = false\n                model.page = .recommendInstance\n            }\n            .fontWeight(.semibold)\n            .imageScale(.large)\n            .labelStyle(.iconOnly)\n            .padding()\n        }\n        .frame(maxHeight: .infinity)\n    }\n    \n    @ViewBuilder\n    var textFieldView: some View {\n        HStack(spacing: 0) {\n            Text(verbatim: \"@\")\n                .foregroundStyle(.secondary)\n            TextField(\"Username\", text: $username, prompt: Text(verbatim: \"\"))\n                .autocorrectionDisabled()\n                .textInputAutocapitalization(.never)\n                .submitLabel(.done)\n                .onSubmit {}\n                .focused($focused)\n                .onAppear {\n                    focused = true\n                }\n        }\n        .padding()\n        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: 16))\n        .task(id: username) {\n            do {\n                usernameValidity = nil\n                if !username.isEmpty {\n                    try await Task.sleep(for: .seconds(0.5))\n                }\n                usernameValidity = try await model.instance?.usernameIsValidForNewAccount?(username)\n            } catch ApiClientError.cancelled {\n                // no-op\n            } catch {\n                handleError(error)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var nextButtonView: some View {\n        Button(action: submit) {\n            Text(\"Next\")\n                .frame(maxWidth: .infinity)\n                .padding(.vertical, 10)\n                .opacity(usernameValidity == nil ? 0 : 1)\n                .overlay {\n                    if usernameValidity == nil {\n                        ProgressView()\n                            .tint(.themedSecondary)\n                    }\n                }\n        }\n        .buttonStyle(.borderedProminent)\n        .buttonBorderShape(.roundedRectangle(radius: 16))\n        .disabled(usernameValidity != .available)\n    }\n\n    @ViewBuilder\n    var validityWarningView: some View {\n        Text(validityWarningText)\n            .font(.footnote)\n            .foregroundStyle(.secondary)\n            .lineLimit(2, reservesSpace: true)\n    }\n    \n    var validityWarningText: String {\n        guard let usernameValidity else { return \" \" }\n        if usernameValidity != .available {\n            return String(localized: usernameValidity.label)\n        } else {\n            return \" \"\n        }\n    }\n    \n    func submit() {\n        guard usernameValidity == .available else { return }\n        model.username = username\n        model.page = .email\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/Onboarding/OnboardingView.swift",
    "content": "//\n//  OnboardingView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-05-19.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct OnboardingView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.colorScheme) var colorScheme\n    @Environment(\\.palette) var palette\n    \n    @State var model = OnboardingModel()\n\n    var body: some View {\n        VStack {\n            switch model.page {\n            case .recommendInstance:\n                OnboardingRecommendInstanceView(instance: model.instance) { model.page = .username }\n                    .transition(.blurReplace)\n            case .email:\n                OnboardingEmailView()\n                    .transition(.blurReplace)\n            case .username:\n                OnboardingUsernameView()\n                    .transition(.blurReplace)\n            }\n        }\n        .animation(.easeOut(duration: 0.2), value: model.page)\n        .animation(.bouncy, value: model.instance?.id)\n        .frame(maxWidth: .infinity, maxHeight: .infinity)\n        .background {\n            VStack {\n                switch model.page {\n                case .recommendInstance:\n                    image\n                default:\n                    ThemedColor.themedGroupedBackground.resolve(with: palette)\n                }\n            }\n            .ignoresSafeArea(.container, edges: .top)\n            .animation(.easeOut(duration: 0.2), value: model.page)\n        }\n        .onAppear {\n            Task {\n                let startTime = Date.now\n                let stub = InstanceStub(api: appState.firstApi, actorId: .init(url: URL(string: \"https://lemmy.world\")!)!)\n                let instance = try await stub.getInstance()\n                try await Task.sleep(for: .seconds(Date.now.advanced(by: 0.1).timeIntervalSince(startTime)))\n                model.instance = instance\n            }\n        }\n        .ignoresSafeArea(.all, edges: .bottom)\n        .environment(model)\n    }\n    \n    @ViewBuilder\n    var image: some View {\n        if colorScheme == .dark {\n            Image(\"background.earth\")\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .frame(maxHeight: .infinity, alignment: .bottom)\n                .background(.black)\n        } else {\n            Image(\"background.trees\")\n                .resizable()\n                .aspectRatio(contentMode: .fill)\n                .frame(maxHeight: .infinity, alignment: .top)\n                .blur(radius: 5, opaque: true)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/SignUpView+EmailConfirmationView.swift",
    "content": "//\n//  SignUpView+EmailConfirmationView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 07/09/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension SignUpView {\n    struct EmailConfirmationView: View {\n        @Environment(NavigationLayer.self) var navigation\n        @Environment(\\.scenePhase) var scenePhase\n        \n        private var timer = Timer.publish(every: 5, tolerance: 0.5, on: .main, in: .common)\n            .autoconnect()\n        \n        let api: ApiClient\n        let email: String\n        let username: String\n        let password: String\n        \n        init(api: ApiClient, email: String, username: String, password: String) {\n            self.api = api\n            self.email = email\n            self.username = username\n            self.password = password\n        }\n        \n        var body: some View {\n            VStack(spacing: Constants.main.doubleSpacing) {\n                Image(icon: .general.email)\n                    .resizable()\n                    .aspectRatio(contentMode: .fit)\n                    .frame(height: 100)\n                    .foregroundStyle(.themedAccent)\n                    .padding(.bottom)\n                Text(\"We sent an email to \\(email) to verify your email address and activate your account.\")\n                    .font(.title2)\n                    .fontWeight(.semibold)\n                Text(\"Click on the link in the email to continue.\")\n                ProgressView()\n                    .tint(.themedSecondary)\n                    .controlSize(.large)\n            }\n            .multilineTextAlignment(.center)\n            .padding()\n            .onDisappear {\n                timer.upstream.connect().cancel()\n            }\n            .onReceive(timer) { _ in\n                Task { await attemptToLogIn() }\n            }\n        }\n        \n        func attemptToLogIn() async {\n            do {\n                let token = try await api.getAccountToken(\n                    usernameOrEmail: username,\n                    password: password,\n                    totpToken: nil\n                )\n                let account = try await AccountsTracker.main.logIn(username: username, url: api.baseUrl, token: token)\n                navigation.dismissSheet()\n                AppState.main.changeAccount(to: account)\n            } catch let ApiClientError.response(response, _) where response.emailNotVerified || response.registrationApplicationIsPending {\n                // no-op\n            } catch {\n                handleError(error)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/SignUpView+Logic.swift",
    "content": "//\n//  SignUpView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 06/09/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension SignUpView {\n    var canSubmit: Bool {\n        guard let captchaEnabled = instance.captchaEnabled.value,\n              let applicationQuestion = instance.applicationQuestion.value else { return false }\n        return !(captchaEnabled && captchaAnswer.isEmpty)\n            && usernameValidity == .valid\n            && (applicationQuestion == nil || !applicationQuestionResponse.isEmpty)\n            && (captcha == nil || !captchaAnswer.isEmpty)\n            && password == confirmPassword\n            && password.count >= 10\n    }\n    \n    func checkUsernameValidity(_ instance: Instance) async {\n        if username.count < 3 {\n            usernameValidity = .tooShort\n            return\n        }\n        if (try? /[a-z_\\d]*/.wholeMatch(in: username)) == nil {\n            usernameValidity = .invalidCharacters\n            return\n        }\n        usernameValidity = .checking\n        do {\n            if username.isEmpty { return }\n            do {\n                try await Task.sleep(for: .seconds(0.2))\n                _ = try await instance.guestApi.getPerson(username: username)\n                usernameValidity = .taken\n            } catch ApiClientError.noEntityFound {\n                usernameValidity = .valid\n            } catch {\n                handleError(error, silent: true)\n            }\n        }\n    }\n    \n    func submit() async {\n        submitting = true\n        do {\n            let response = try await instance.guestApi.signUp(\n                username: username,\n                password: password,\n                confirmPassword: confirmPassword,\n                showNsfw: showNsfw,\n                email: email.isEmpty ? nil : email,\n                captcha: captcha,\n                captchaAnswer: captchaAnswer.isEmpty ? nil : captchaAnswer,\n                applicationQuestionResponse: applicationQuestionResponse.isEmpty ? nil : applicationQuestionResponse\n            )\n            switch response {\n            case let .canLogIn(token: token):\n                let account = try await AccountsTracker.main.logIn(\n                    username: username,\n                    url: instance.guestApi.baseUrl,\n                    token: token\n                )\n                AppState.main.changeAccount(to: account)\n                navigation.dismissSheet()\n                return\n            case let .cannotLogIn(reasons: reasons):\n                if reasons.contains(.awaitingApproval) {\n                    signInResult = .awaitingApproval\n                } else {\n                    signInResult = .awaitingEmail\n                }\n            }\n        } catch {\n            handleError(error)\n        }\n        submitting = false\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/SignUpView+Views.swift",
    "content": "//\n//  SignUpView+Views.swift\n//  Mlem\n//\n//  Created by Sjmarf on 06/09/2024.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nextension SignUpView {\n    @ViewBuilder\n    var approvalInfo: some View {\n        VStack(spacing: Constants.main.doubleSpacing) {\n            Image(icon: .lemmy.send)\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .frame(height: 100)\n                .foregroundStyle(.themedAccent)\n                .padding(.bottom)\n            Text(\"Application submitted!\")\n                .font(.title2)\n                .fontWeight(.semibold)\n            if email.isEmpty {\n                Text(\"Once approved, you'll be able to log in to your account from the Settings tab.\")\n            } else {\n                Text(\n                    // swiftlint:disable:next line_length\n                    \"You'll receive an email once your application has been approved. Once approved, you can log in to your account from the Settings tab.\"\n                )\n            }\n            Button(\"Done\") {\n                navigation.dismissSheet()\n            }\n            .buttonStyle(SubmitButtonStyle())\n        }\n        .multilineTextAlignment(.center)\n        .padding()\n    }\n    \n    @ViewBuilder\n    var header: some View {\n        Section {\n            VStack {\n                CircleCroppedImageView(instance, frame: 50)\n                Text(instance.displayName)\n                    .font(.title)\n                    .bold()\n            }\n            .frame(maxWidth: .infinity)\n            .listRowBackground(Color.clear)\n            .listRowInsets(.init())\n        } header: {\n            // https://stackoverflow.com/a/78618856/17629371\n            Spacer(minLength: 0).listRowInsets(EdgeInsets())\n        }\n    }\n    \n    @ViewBuilder\n    var usernameSection: some View {\n        Section(\"Username\") {\n            HStack {\n                TextField(\"Username\", text: $username, prompt: Text(\n                    \"john_doe\",\n                    comment: \"Translate this into a similar placeholder name in your language.\"\n                ))\n                .focused($focused, equals: .username)\n                .onSubmit { focused = .email }\n                .autocorrectionDisabled()\n                .textInputAutocapitalization(.never)\n                .task(id: username) {\n                    await checkUsernameValidity(instance)\n                }\n                Group {\n                    if !username.isEmpty {\n                        switch usernameValidity {\n                        case .checking:\n                            ProgressView()\n                                .tint(.themedSecondary)\n                        case .valid:\n                            Image(icon: .general.success)\n                                .foregroundStyle(.themedPositive)\n                        case .taken, .tooShort, .invalidCharacters:\n                            Image(icon: .general.failure)\n                                .foregroundStyle(.themedNegative)\n                        }\n                    }\n                }\n                .symbolVariant(.circle.fill)\n            }\n        } footer: {\n            if username.isEmpty {\n                Text(\"Choose wisely - you cannot change this later.\")\n            } else {\n                Group {\n                    switch usernameValidity {\n                    case .invalidCharacters:\n                        Text(\"Username can only contain lowercase letters, numbers and underscores.\")\n                    case .tooShort:\n                        Text(\"Username must be 3 or more characters.\")\n                    case .taken:\n                        Text(\"This username is taken.\")\n                    default:\n                        Text(verbatim: \"\")\n                    }\n                }\n                .foregroundStyle(.themedWarning)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var emailSection: some View {\n        Section(\"Email\") {\n            TextField(\n                \"Email\",\n                text: $email,\n                // Converting to a String avoids this being rendered as a link\n                prompt: Text(String(\n                    localized: \"john_doe@example.com\",\n                    // swiftlint:disable:next line_length\n                    comment: \"Translate \\\"john_doe\\\" into the equivalent placeholder name in your language, and \\\"example.com\\\" into a suitable example domain for your locale.\"\n                ))\n            )\n            .focused($focused, equals: .email)\n            .onSubmit { focused = .password }\n            .autocorrectionDisabled()\n            .textInputAutocapitalization(.never)\n            .keyboardType(.emailAddress)\n        } footer: {\n            ExpectedView(instance.emailVerificationRequired) { emailVerificationRequired in\n                if emailVerificationRequired {\n                    Text(\"You are required to provide an email on this instance.\")\n                } else {\n                    Text(\"This field is optional.\")\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var passwordSection: some View {\n        if let applicationQuestion = instance.applicationQuestion.value {\n            Section(\"Password\") {\n                SecureField(\"Password\", text: $password)\n                    .focused($focused, equals: .password)\n                    .onSubmit { focused = .confirmPassword }\n                SecureField(\"Confirm Password\", text: $confirmPassword)\n                    .focused($focused, equals: .confirmPassword)\n                    .onSubmit {\n                        focused = applicationQuestion == nil ? .captchaAnswer : .applicationQuestionResponse\n                    }\n            } footer: {\n                if !confirmPassword.isEmpty, password != confirmPassword {\n                    Text(\"Passwords don't match.\")\n                        .foregroundStyle(.themedWarning)\n                } else if password.count < 10 {\n                    // Using interpolation so we don't have to change the localization if this changes\n                    Text(\"Password must be \\(10) characters or more.\")\n                        .foregroundStyle(confirmPassword.isEmpty ? .themedSecondary : .themedWarning)\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var applicationQuestionSection: some View {\n        if let applicationQuestion = instance.applicationQuestion.value as? String {\n            Section {\n                Markdown(applicationQuestion, configuration: .default(palette: palette))\n                    .padding(.vertical, 8)\n                TextField(\"Your Answer...\", text: $applicationQuestionResponse, axis: .vertical)\n                    .focused($focused, equals: .applicationQuestionResponse)\n                    .lineLimit(8, reservesSpace: true)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var captchaSection: some View {\n        if let captchaImage = captcha?.image {\n            Section {\n                captchaImage\n                    .resizable()\n                    .aspectRatio(contentMode: .fit)\n                    .frame(maxWidth: 500, alignment: .leading)\n                    .listRowInsets(.init())\n                TextField(\"Answer...\", text: $captchaAnswer)\n                    .focused($focused, equals: .captchaAnswer)\n                    .onSubmit { focused = .captchaAnswer }\n                    .autocorrectionDisabled()\n                    .textInputAutocapitalization(.never)\n            } footer: {\n                Button(\"Try a different Captcha...\") {\n                    Task {\n                        do {\n                            captcha = try await instance.guestApi.getCaptcha()\n                            captchaAnswer = \"\"\n                        } catch {\n                            handleError(error)\n                        }\n                    }\n                }\n                .foregroundStyle(.themedAccent)\n                .font(.footnote)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var applicationQuestionWarning: some View {\n        Section {\n            HStack {\n                Image(icon: .general.warning)\n                    .font(.title2)\n                    .imageScale(.large)\n                Text(\"To join this instance, you need to create an application and wait to be accepted.\")\n            }\n            .foregroundStyle(.themedCaution)\n            .listRowBackground(\n                RoundedRectangle(cornerRadius: UIDevice.isIos26 ? 26 : 10)\n                    .stroke(.themedCaution, lineWidth: 3)\n                    .background(.themedCaution.opacity(0.1))\n                    .clipShape(RoundedRectangle(cornerRadius: 10))\n            )\n        }\n    }\n    \n    @ViewBuilder\n    var submitButton: some View {\n        ExpectedView(instance.applicationQuestion) { applicationQuestion in\n            Button(String(localized: submitLabel(applicationQuestion))) {\n                Task { await submit() }\n            }\n            .buttonStyle(SubmitButtonStyle())\n            .disabled(!canSubmit)\n        }\n    }\n    \n    private func submitLabel(_ applicationQuestion: String?) -> LocalizedStringResource {\n        if submitting { return \"Submitting...\" }\n        return applicationQuestion == nil ? \"Sign Up\" : \"Submit Application\"\n    }\n}\n\nprivate struct SubmitButtonStyle: ButtonStyle {\n    @Environment(\\.isEnabled) private var isEnabled: Bool\n    \n    func makeBody(configuration: Configuration) -> some View {\n        configuration.label\n            .padding(12)\n            .frame(maxWidth: .infinity)\n            .foregroundStyle(.themedContrastingLabel)\n            .background(isEnabled ? .themedAccent : .themedSecondary, in: .rect(cornerRadius: 10))\n            .opacity(opacity(isPressed: configuration.isPressed))\n    }\n    \n    func opacity(isPressed: Bool) -> CGFloat {\n        if !isEnabled { return 0.5 }\n        return isPressed ? 0.8 : 1\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Login/SignUpView.swift",
    "content": "//\n//  SignUpView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 05/09/2024.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct SignUpView: View {\n    enum UsernameValidity {\n        case checking, valid, tooShort, taken, invalidCharacters\n    }\n\n    enum FocusedField: Hashable {\n        case username, email, password, confirmPassword, applicationQuestionResponse, captchaAnswer\n    }\n\n    enum SignInResult {\n        case awaitingEmail, awaitingApproval\n    }\n\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.palette) var palette\n    @Environment(\\.isRootView) var isRootView\n    @Environment(\\.scenePhase) var scenePhase\n    \n    @State var instance: Instance\n    @State var upgradeState: LoadingState = .idle\n    @State var captcha: Captcha?\n    \n    @State var username: String = \"\"\n    @State var email: String = \"\"\n    @State var password: String = \"\"\n    @State var confirmPassword: String = \"\"\n    @State var applicationQuestionResponse: String = \"\"\n    @State var showNsfw: Bool = false\n    @State var captchaAnswer: String = \"\"\n    \n    @State var usernameValidity: UsernameValidity = .tooShort\n    @State var submitting: Bool = false\n    @FocusState var focused: FocusedField?\n    \n    @State var signInResult: SignInResult?\n    \n    var body: some View {\n        VStack {\n            if let captchaEnabled = instance.captchaEnabled.value,\n               let registrationMode = instance.registrationMode.value,\n               captcha != nil || !captchaEnabled {\n                switch signInResult {\n                case .awaitingEmail:\n                    EmailConfirmationView(\n                        api: instance.guestApi,\n                        email: email,\n                        username: username,\n                        password: password\n                    )\n                case .awaitingApproval:\n                    approvalInfo\n                case nil:\n                    if registrationMode == .closed {\n                        Text(\"Registrations are closed on this instance.\")\n                    } else {\n                        content\n                    }\n                }\n            } else {\n                ProgressView()\n                    .tint(.themedSecondary)\n            }\n        }\n        .task(id: instance.captchaEnabled.value) {\n            if captcha == nil, instance.captchaEnabled.value ?? false {\n                do {\n                    captcha = try await instance.guestApi.getCaptcha()\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n        .animation(.easeOut(duration: 0.1), value: signInResult)\n        .frame(maxWidth: .infinity, maxHeight: .infinity)\n        .background(.themedGroupedBackground)\n        .presentationBackground(.themedGroupedBackground)\n        .navigationBarTitleDisplayMode(.inline)\n        .toolbar {\n            if navigation.isInsideSheet, isRootView {\n                ToolbarItem(placement: .topBarLeading) {\n                    CloseButtonView(ios18Label: .cancel) {\n                        navigation.dismissSheet()\n                    }\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n        var content: some View {\n        Form {\n            header\n            if instance.applicationQuestion.value is String {\n                applicationQuestionWarning\n            }\n            usernameSection\n            emailSection\n            passwordSection\n            applicationQuestionSection\n            Section {\n                Toggle(\"Show NSFW Content\", isOn: $showNsfw)\n                    .tint(.themedWarning)\n            }\n            captchaSection\n            Section {\n                submitButton\n                    .listRowBackground(Color.clear)\n                    .listRowInsets(.init())\n            }\n        }\n        .environment(\\.defaultMinListHeaderHeight, 0)\n        .scrollDismissesKeyboard(.interactively)\n        .disabled(submitting)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/MlemApp.swift",
    "content": "//\n//  MlemApp.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-02-21.\n//\n\nimport AVFAudio\nimport Nuke\nimport SDWebImageWebPCoder\nimport SwiftUI\nimport Media\n\n/// Root view for the app\n@main\nstruct MlemApp: App {\n    init() {\n        var imageConfig = ImagePipeline.Configuration.withDataCache(name: \"main\", sizeLimit: Constants.main.cacheSize)\n        imageConfig.dataLoadingQueue = OperationQueue(maxConcurrentCount: 8)\n        imageConfig.imageDecodingQueue = OperationQueue(maxConcurrentCount: 8) // Let's use those CORES\n        imageConfig.imageDecompressingQueue = OperationQueue(maxConcurrentCount: 8)\n        \n        // TODO: rate limiting\n        ImagePipeline.shared = ImagePipeline(configuration: imageConfig)\n        \n        // video handling\n        ImageDecoderRegistry.shared.register(MlemVideoDecoder.init)\n        \n        // webp handling\n        ImageDecoderRegistry.shared.register(NukeWebpBridgeDecoder.init)\n        SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)\n        \n        // caching\n        URLCache.shared = Constants.main.urlCache\n        \n        // set up audio\n        do {\n            try AVAudioSession.sharedInstance().setCategory(.playback, options: [.mixWithOthers])\n        } catch {\n            handleError(error)\n        }\n    }\n    \n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n        }\n    }\n}\n\nextension OperationQueue {\n    convenience init(maxConcurrentCount: Int) {\n        self.init()\n        self.maxConcurrentOperationCount = maxConcurrentCount\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Components/FeedWelcomeView.swift",
    "content": "//\n//  FeedWelcomeView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 22/09/2024.\n//\n\nimport SwiftUI\n\nstruct FeedWelcomeView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    @Setting(\\.tip_feedWelcomePrompt) var showWelcomePrompt\n    \n    var body: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            HStack(spacing: Constants.main.standardSpacing) {\n                VStack(alignment: .leading) {\n                    Text(\"Welcome to Lemmy!\")\n                        .fontWeight(.semibold)\n                    Text(\n                        // swiftlint:disable:next line_length\n                        \"You are browsing \\(appState.firstApi.host) as a guest. If you'd like to vote or reply, you'll need to log in or sign up.\"\n                    )\n                    .font(.footnote)\n                }\n            }\n            .foregroundStyle(.themedAccent)\n            HStack(spacing: Constants.main.standardSpacing) {\n                Button {\n                    navigation.openSheet(.logIn(.pickInstance))\n                } label: {\n                    Text(\"Log In\")\n                        .frame(maxWidth: 400)\n                        .padding(.vertical, 4)\n                }\n                Button {\n                    navigation.openSheet(.signUp())\n                } label: {\n                    Text(\"Sign Up\")\n                        .frame(maxWidth: 400)\n                        .padding(.vertical, 4)\n                }\n            }\n            .buttonStyle(.borderedProminent)\n        }\n        .padding(Constants.main.standardSpacing)\n        .background(.themedAccent.opacity(0.2), in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .overlay(alignment: .topTrailing) {\n            Button(\"Dismiss\", icon: .general.close) {\n                showWelcomePrompt = false\n            }\n            .symbolVariant(.circle.fill)\n            .symbolRenderingMode(.hierarchical)\n            .labelStyle(.iconOnly)\n            .fontWeight(.semibold)\n            .padding(4)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Components/HiddenReadBannerView.swift",
    "content": "//\n//  HiddenReadBannerView.swift\n//  Mlem\n//\n//  Created by Bedir Ekim on 2026-02-17.\n//\n\nimport SwiftUI\n\nstruct HiddenReadBannerView: View {\n    @Setting(\\.feed_showRead) var showRead\n\n    let onDismiss: () -> Void\n\n    var body: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            Text(\"Looking for something? Read posts are hidden.\")\n                .font(.subheadline)\n                .foregroundStyle(.themedAccent)\n                .frame(maxWidth: .infinity, alignment: .leading)\n            HStack(spacing: Constants.main.standardSpacing) {\n                Button {\n                    showRead = true\n                } label: {\n                    Text(\"Show Read\")\n                        .frame(maxWidth: 400)\n                        .padding(.vertical, 4)\n                }\n                .buttonStyle(.borderedProminent)\n                Button {\n                    onDismiss()\n                } label: {\n                    Text(\"Dismiss\")\n                        .frame(maxWidth: 400)\n                        .padding(.vertical, 4)\n                }\n                .buttonStyle(.bordered)\n            }\n        }\n        .padding(Constants.main.standardSpacing)\n        .background(.themedAccent.opacity(0.2), in: .rect(cornerRadius: Constants.main.standardSpacing))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Components/TileScoreView.swift",
    "content": "//\n//  TileScoreView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-19.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nstruct TileScoreView: View {\n    let saved: ExpectedValue<Bool>\n    let votes: ExpectedValue<VotesModel>\n    \n    var body: some View {\n        if let saved = saved.value, let votes = votes.value {\n            Group {\n                postTag(active: saved, icon: .lemmy.saved.representingState(active: true), color: .themedSave) + // saved status\n                Text(verbatim: saved ? \" \" : \"\") + // spacing after save\n                Text(Image(systemName: votes.iconName)) + // vote status\n                Text(verbatim: \" \\(votes.total.abbreviated)\")\n            }\n            .lineLimit(1)\n            .font(.caption)\n            .foregroundStyle(votes.iconColor)\n            .contentShape(.rect)\n        }\n    }\n}\n\nextension TileScoreView {\n    init(_ interactable: any InteractableProviding) {\n        self.saved = interactable.saved\n        self.votes = interactable.votes\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Components/UpdateBannerView.swift",
    "content": "//\n//  UpdateBannerView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-29.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct UpdateBannerView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.palette) var palette\n    \n    @AppStorage(\"lastTestFlightUpdate\") var lastTestFlightUpdate: URL?\n    \n    @State var isLoading: Bool = false\n    \n    let url: URL\n    \n    var body: some View {\n        HStack {\n            Text(\"TestFlight updated!\")\n                .fontWeight(.semibold)\n                .foregroundStyle(.themedAccent)\n                .padding(.leading, 5)\n            Spacer()\n            Button(action: submit) {\n                Text(\"What's New?\")\n                    .padding(.vertical, 4)\n                    .opacity(isLoading ? 0 : 1)\n                    .overlay {\n                        if isLoading {\n                            ProgressView()\n                                .tint(.themedContrastingLabel)\n                        }\n                    }\n            }\n            .buttonStyle(.borderedProminent)\n        }\n        .padding(Constants.main.standardSpacing)\n        .background(.themedAccent.opacity(0.2))\n        // This avoid being partially transparent when context menu is open\n        .background(.themedBackground)\n        .clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n        .quickSwipes(trailing: [\n            BasicAction(\n                id: \"dismissTestFlightUpdatePopup\",\n                appearance: .init(label: \"Dismiss\", color: .themedNegative, icon: Icons.close),\n                callback: dismiss\n            )\n        ])\n        .contextMenu {\n            Button(\"Dismiss\", icon: .general.close) {\n                DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) {\n                    dismiss()\n                }\n            }\n        }\n        .onChange(of: navigation.path) {\n            if case .post(let post, _, _, _) = navigation.path.last,\n               post.allResolvableUrls.contains(url) {\n                dismiss()\n            }\n        }\n    }\n    \n    func dismiss() {\n        lastTestFlightUpdate = url\n    }\n    \n    func submit() {\n        isLoading = true\n        Task {\n            do {\n                let announcementPost = try await appState.firstApi.getPost(url: url)\n                navigation.push(.post(announcementPost))\n                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                    dismiss()\n                }\n            } catch {\n                handleError(error)\n                isLoading = false\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/FeedCommentView.swift",
    "content": "//\n//  FeedCommentView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-07-21.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nstruct FeedCommentView<EmbeddedContent: View>: View {\n    @Environment(AppState.self) private var appState\n    @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker?\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.reportContext) var reportContext: Report?\n    \n    @Setting(\\.post_size) var settingsPostSize\n    @Setting(\\.comment_compact) var compactComments\n    @Setting(\\.interactionBar_comment) var commentInteractionBar\n    @Setting(\\.interactionBar_commentReport) var commentReportInteractionBar\n    @Setting(\\.interactionBar_alternateReportLayout) var alternateInteractionBarLayoutForReports: Bool\n\n    let comment: Comment\n    var overriddenSize: PostSize?\n    @ViewBuilder var embeddedContent: () -> EmbeddedContent\n    \n    init(\n        comment: Comment,\n        overriddenSize: PostSize? = nil,\n        @ViewBuilder embeddedContent: @escaping () -> EmbeddedContent = { EmptyView() }\n    ) {\n        self.comment = comment\n        self.overriddenSize = overriddenSize\n        self.embeddedContent = embeddedContent\n    }\n    \n    var postSize: PostSize { overriddenSize ?? settingsPostSize }\n    \n    var showCompactPostContext: Bool {\n        postSize == .compact || compactComments\n    }\n    \n    var body: some View {\n        content\n            .contentShape(.interaction, .rect)\n            .quickSwipes(\n                comment: comment,\n                configuration: interactionBarConfiguration\n            )\n            .contextMenu(comment: comment)\n            .paletteBorder(cornerRadius: postSize.cornerRadius)\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        if postSize.tiled {\n            TileCommentView(comment: comment)\n        } else {\n            CommentView(comment: comment, inFeed: true, embeddedContent: embeddedContent)\n        }\n    }\n    \n    func headerUrl(post: Post) -> URL? {\n        switch post.type {\n        case let .media(url), let .embedded(url, _): url\n        case let .link(link): link.thumbnail\n        default: nil\n        }\n    }\n    \n    var interactionBarConfiguration: CommentBarConfiguration {\n        if reportContext != nil, alternateInteractionBarLayoutForReports {\n            return commentReportInteractionBar\n        }\n        return commentInteractionBar\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/TileCommentView.swift",
    "content": "//\n//  TileCommentView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-07-19.\n//\n\nimport Foundation\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct TileCommentView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.palette) var palette\n    @Environment(\\.parentFrameWidth) var parentFrameWidth: CGFloat\n    \n    let comment: Comment\n    \n    @ScaledMetric(relativeTo: .footnote) var titleHeight: CGFloat = 36 // (2 * .footnote height), including built-in spacing\n    @ScaledMetric(relativeTo: .caption) var communityHeight: CGFloat = 16 // .caption height, including built-in spacing\n    \n    let contentHeightModifier: CGFloat = 33\n    // width cannot go below contentHeightModifier so contentWidth is never negative\n    var width: CGFloat { max(contentHeightModifier, (parentFrameWidth - (Constants.main.standardSpacing * 3)) / 2) }\n    var contentHeight: CGFloat { width - 33 }\n    var frameHeight: CGFloat { width + titleHeight + communityHeight + 17 }\n    // Padding math\n    // Need to satisfy: padding + contentHeightModifier = 17\n    //\n    // VStack spacing = (2 * Constants.main.standardSpacing) = 20\n    // External padding = (2 * Constants.main.standardSpacing) = 20\n    // Internal titleSection padding = (2 * Constants.main.halfSpacing) = 10\n    //\n    // Total padding = 50\n    // 50 + contentHeightModifier = 17\n    // contentHeightModifier = -33\n    \n    var body: some View {\n        content\n            .frame(width: width, height: frameHeight)\n            .background(.themedSecondaryGroupedBackground)\n            .clipShape(.rect(cornerRadius: Constants.main.largeItemCornerRadius))\n            .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.largeItemCornerRadius))\n    }\n    \n    var content: some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            ExpectedView(comment.post) { post in\n                titleSection(post: post)\n                    .typesettingLanguage(.init(languageCode: .english))\n                    .frame(height: titleHeight, alignment: .topLeading)\n                    .padding(Constants.main.halfSpacing)\n                    .background {\n                        RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius)\n                            .fill(.themedTertiaryGroupedBackground)\n                    }\n                    .paletteBorder(cornerRadius: Constants.main.smallItemCornerRadius)\n            }\n            MarkdownText(comment.content, configuration: .caption(palette: palette))\n                .frame(height: contentHeight, alignment: .top)\n                .clipped()\n\n            communityAndInfo\n        }\n        .padding(Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    func titleSection(post: Post) -> some View {\n        Text(post.title)\n            .lineLimit(2)\n            .foregroundStyle(.themedSecondary)\n            .font(.footnote)\n            .fontWeight(.semibold)\n            .frame(maxWidth: .infinity, alignment: .topLeading)\n    }\n    \n    var replyIcon: Text {\n        Text(Image(icon: .lemmy.reply))\n            .foregroundStyle(.themedAccent)\n    }\n    \n    var communityAndInfo: some View {\n        HStack(spacing: 6) {\n            if let communityName = comment.community.value?.name {\n                Text(communityName)\n                    .lineLimit(1)\n                    .font(.caption)\n                    .fontWeight(.semibold)\n                    .foregroundStyle(.themedSecondary)\n            }\n            \n            Spacer()\n            \n            score\n        }\n        .frame(maxWidth: .infinity)\n    }\n    \n    var score: some View {\n        Menu {\n            ForEach(comment.allMenuActions(appState: appState, navigation: navigation), id: \\.id) { action in\n                MenuButton(action: action)\n            }\n        } label: {\n            TileScoreView(comment)\n        }\n        .onTapGesture {}\n        .popupAnchor()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Header/FeedDescription.swift",
    "content": "//\n//  SubscribedFeedIcon.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-27.\n//\n\nimport Foundation\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct FeedDescription {\n    var label: LocalizedStringResource\n    var subtitle: LocalizedStringResource\n    var color: ThemedColor\n    var icon: Icon\n    var iconScaleFactor: CGFloat\n    \n    static var all: FeedDescription = .init(\n        label: \"All\",\n        subtitle: \"Posts from all federated instances\",\n        color: .themedFederatedFeed,\n        icon: .lemmy.federatedFeed,\n        iconScaleFactor: 0.6\n    )\n    \n    static var local: FeedDescription {\n        .init(\n            label: \"Local\",\n            subtitle: \"Posts from \\(AppState.main.firstApi.host) communities\",\n            color: .themedLocalFeed,\n            icon: .lemmy.localFeed,\n            iconScaleFactor: 0.55\n        )\n    }\n    \n    static var subscribed: FeedDescription = .init(\n        label: \"Subscribed\",\n        subtitle: \"Posts from communities you subscribe to\",\n        color: .themedSubscribedFeed,\n        icon: .lemmy.subscribedFeed,\n        iconScaleFactor: 0.5\n    )\n    \n    static var moderated: FeedDescription = .init(\n        label: \"Moderated\",\n        subtitle: \"Posts from communities you moderate\",\n        color: .themedModeratedFeed,\n        icon: .lemmy.moderatedFeed,\n        iconScaleFactor: 0.5\n    )\n    \n    static var saved: FeedDescription = .init(\n        label: \"Saved\",\n        subtitle: \"Your saved posts and comments\",\n        color: .themedSavedFeed,\n        icon: .lemmy.savedFeed,\n        iconScaleFactor: 0.55\n    )\n\n    static var popular: FeedDescription = .init(\n        label: \"Popular\",\n        subtitle: \"Posts from popular communities\",\n        color: .themedPopularFeed,\n        icon: .lemmy.popularFeed,\n        iconScaleFactor: 0.55\n    )\n\n    static var suggested: FeedDescription = .init(\n        label: \"Suggested\",\n        subtitle: \"A selection of communities curated by \\(AppState.main.firstApi.host) admins\",\n        color: .themedSuggestedFeed,\n        icon: .lemmy.suggestedFeed,\n        iconScaleFactor: 0.55\n    )\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Header/FeedHeaderView.swift",
    "content": "//\n//  FeedHeaderView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-01-28.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct FeedHeaderView<ImageContent: View>: View {\n    @Environment(AppState.self) var appState\n    \n    enum DropdownStyle {\n        case disabled\n        case enabled(showBadge: Bool)\n    }\n    \n    // Using `Text` rather than `String` here to avoid having to make 4 initializers to handle\n    // all permutations of `String` and `LocalizedStringResource` for `title and `subtitle`.\n    let title: Text\n    let subtitle: Text\n    \n    let image: ImageContent\n    let dropdownStyle: DropdownStyle\n    \n    init(\n        title: Text,\n        subtitle: Text,\n        dropdownStyle: DropdownStyle,\n        @ViewBuilder image: () -> ImageContent\n    ) {\n        self.title = title\n        self.subtitle = subtitle\n        self.dropdownStyle = dropdownStyle\n        self.image = image()\n    }\n    \n    init(\n        feedDescription: FeedDescription,\n        customSubtitle: LocalizedStringResource? = nil,\n        dropdownStyle: DropdownStyle\n    ) where ImageContent == FeedIconView {\n        self.title = Text(feedDescription.label)\n        self.subtitle = Text(customSubtitle ?? feedDescription.subtitle)\n        self.image = FeedIconView(feedDescription: feedDescription, size: Constants.main.feedHeaderSize)\n        self.dropdownStyle = dropdownStyle\n    }\n    \n    var body: some View {\n        VStack(spacing: 0) {\n            HStack(alignment: .center, spacing: Constants.main.standardSpacing) {\n                image\n                    .frame(width: Constants.main.feedHeaderSize, height: Constants.main.feedHeaderSize)\n                    .padding(.leading, Constants.main.standardSpacing)\n                    \n                VStack(alignment: .leading, spacing: 0) {\n                    HStack(spacing: Constants.main.halfSpacing) {\n                        title\n                            .lineLimit(1)\n                            .minimumScaleFactor(0.01)\n                            .fontWeight(.semibold)\n                            .foregroundStyle(.themedPrimary)\n                        \n                        if case let .enabled(showBadge) = dropdownStyle {\n                            Image(icon: .general.dropDown)\n                                .foregroundStyle(.themedSecondary)\n                                .overlay(alignment: .topTrailing) {\n                                    if showBadge {\n                                        Circle()\n                                            .frame(width: 6, height: 6)\n                                            .foregroundStyle(.themedWarning)\n                                    }\n                                }\n                        }\n                    }\n                    .font(.title2)\n                        \n                    subtitle\n                        .font(.footnote)\n                        .foregroundStyle(.themedSecondary)\n                }\n                .frame(height: Constants.main.feedHeaderSize)\n                .frame(maxWidth: .infinity, alignment: .leading)\n            }\n            .padding(.top, Constants.main.halfSpacing)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Header/FeedIconView.swift",
    "content": "//\n//  FeedIconView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-27.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct FeedIconView: View {\n    @Environment(\\.palette) var palette\n    \n    let feedDescription: FeedDescription\n    let size: CGFloat\n    let scaledSize: CGFloat\n    \n    init(feedDescription: FeedDescription, size: CGFloat) {\n        self.feedDescription = feedDescription\n        self.size = size\n        self.scaledSize = size * feedDescription.iconScaleFactor\n    }\n    \n    var body: some View {\n        Circle()\n            .fill(feedDescription.color.gradient(palette: palette))\n            .frame(width: size, height: size)\n            .overlay {\n                Image(icon: feedDescription.icon)\n                    .resizable()\n                    .symbolVariant(.fill)\n                    .aspectRatio(contentMode: .fit)\n                    .foregroundStyle(.white)\n                    .frame(width: scaledSize, height: scaledSize)\n            }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/CompactPostView.swift",
    "content": "//\n//  CompactPostView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-19.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CompactPostView: View {\n    @Setting(\\.post_thumbnailLocation) var thumbnailLocation\n    @Setting(\\.safety_blurNsfw) var blurNsfw\n    @Setting(\\.a11y_readPostIndicator) var readPostIndicator\n    @Setting(\\.post_showDownvotesCompact) var showDownvotesCompact\n    \n    @Environment(\\.communityContext) var communityContext: Community?\n    @Environment(\\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor\n    \n    @ScaledMetric(relativeTo: .caption) var titleHostHeightLimit: CGFloat = 40\n    \n    let post: Post\n    var requireConsistentHeight: Bool = false\n    \n    var readouts: [PostBarConfiguration.ReadoutType] {\n        var readouts: [PostBarConfiguration.ReadoutType] = [.created]\n        readouts.append(contentsOf: showDownvotesCompact ? [.upvote, .downvote, .comment] : [.score, .comment])\n        readouts.appendIfPresent(post.saved.value ?? false ? .saved : nil)\n        return readouts\n    }\n    \n    var blurred: Bool {\n        switch blurNsfw {\n        case .always: post.nsfw\n        case .outsideCommunity: post.nsfw && !(communityContext?.nsfw ?? false)\n        case .never: false\n        }\n    }\n    \n    var body: some View {\n        content\n            .padding(Constants.main.standardSpacing)\n            .background(.themedSecondaryGroupedBackground)\n            .environment(\\.postContext, post)\n    }\n    \n    var content: some View {\n        HStack(alignment: .top, spacing: Constants.main.standardSpacing) {\n            if thumbnailLocation == .left {\n                ThumbnailImageView(\n                    post: post,\n                    blurred: blurred,\n                    size: .standard,\n                    frame: .init(width: Constants.main.thumbnailSize, height: Constants.main.thumbnailSize)\n                )\n            }\n            \n            VStack(alignment: .leading, spacing: Constants.main.compactSpacing) {\n                HStack(spacing: 4) {\n                    if communityContext != nil {\n                        ExpectedView(post.creator) { creator in\n                            FullyQualifiedLinkView(creator, labelStyle: .small, showAvatar: false)\n                        } placeholder: {\n                            Text(verbatim: .personPlaceholder).redacted(reason: .placeholder)\n                        }\n                    } else {\n                        ExpectedView(post.community) { community in\n                            FullyQualifiedLinkView(community, labelStyle: .small, showAvatar: false)\n                        } placeholder: {\n                            Text(verbatim: .communityPlaceholder).redacted(reason: .placeholder)\n                        }\n                        \n                    }\n                    Spacer()\n                    \n                    if differentiateWithoutColor, readPostIndicator == .checkmark {\n                        ReadCheck(read: post.read)\n                    }\n                    \n                    if post.nsfw {\n                        Image(icon: .lemmy.nsfwTag)\n                            .foregroundStyle(.themedWarning)\n                            .imageScale(.small)\n                    }\n                    \n                    // Allow the tap area to extend outside of the parent HStack a little\n                    PostEllipsisMenus(post: post, size: 20)\n                        .padding(.vertical, -2)\n                }\n                .padding(.bottom, -2)\n                if requireConsistentHeight {\n                    titleAndHostView\n                        .frame(height: titleHostHeightLimit, alignment: .top)\n                } else {\n                    titleAndHostView\n                }\n                InfoStackView(post: post, readouts: readouts, coloredReadouts: .init(PostBarConfiguration.ReadoutType.allCases))\n            }\n            .frame(maxWidth: .infinity)\n            \n            if thumbnailLocation == .right {\n                ThumbnailImageView(\n                    post: post,\n                    blurred: blurred,\n                    size: .standard,\n                    frame: .init(width: Constants.main.thumbnailSize, height: Constants.main.thumbnailSize)\n                )\n            }\n        }\n        .frame(maxWidth: .infinity, alignment: .leading)\n    }\n    \n    @ViewBuilder\n    var titleAndHostView: some View {\n        VStack(alignment: .leading, spacing: Constants.main.compactSpacing) {\n            titleView\n            if let host = post.linkHost {\n                PostLinkHostView(host: host)\n                    .font(.caption)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var titleView: some View {\n        post.taggedTitle(communityContext: communityContext)\n            .symbolVariant(.fill)\n            .multilineTextAlignment(.leading)\n            .imageScale(.small)\n            .foregroundStyle(post.read.value ?? false ? .themedSecondary : .themedPrimary)\n            .font(.subheadline)\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment, .sizeThatFitsLayout) {\n//        DevCompactPostView(post: Post2.mock(.generic))\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/Feed Post Components/CrossPostListView.swift",
    "content": "//\n//  CrossPostListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/09/2024.\n//\n\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CrossPostListView: View {\n    @Environment(AppState.self) private var appState\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(NavigationLayer.self) private var navigation\n    \n    let post: Post\n    \n    @State private var isExpanded: Bool = false\n    \n    var body: some View {\n        // does not use ExpectedView because of padding reasons and because the animation is not necessary\n        if let crossPosts = post.crossPosts.value {\n            content(crossPosts)\n        }\n    }\n    \n    @ViewBuilder\n    // swiftlint:disable:next function_body_length\n    func content(_ crossPosts: [Post]) -> some View {\n        if !crossPosts.isEmpty {\n            VStack(spacing: Constants.main.halfSpacing) {\n                Button {\n                    hapticManager.play(haptic: .gentleInfo, tier: .low)\n                    withAnimation(.easeOut(duration: 0.2)) {\n                        isExpanded.toggle()\n                    }\n                } label: {\n                    HStack {\n                        Image(icon: .lemmy.crosspost)\n                            .foregroundStyle(.themedSecondary)\n                            .fontWeight(.semibold)\n                        Text(\"\\(crossPosts.count) Crossposts...\")\n                        Spacer()\n                        HStack(spacing: 2) {\n                            Image(icon: .lemmy.comment)\n                            Text(String(crossPosts.reduce(0) { $0 + ($1.commentCount.value ?? 0) }))\n                        }\n                        .font(.footnote)\n                        .foregroundStyle(.themedSecondary)\n                    }\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                    .contentShape(.rect)\n                }\n                .buttonStyle(.empty)\n                if isExpanded {\n                    Divider()\n                        .padding(.vertical, 3)\n                    Grid(alignment: .leading) {\n                        ForEach(crossPosts) { crossPost in\n                            GridRow {\n                                ExpectedView(crossPost.community) { community in\n                                    FullyQualifiedLabelView(community, labelStyle: .medium, blurred: crossPost.nsfw)\n                                        .frame(maxWidth: .infinity, alignment: .leading)\n                                } placeholder: {\n                                    Text(verbatim: .communityPlaceholder).redacted(reason: .placeholder)\n                                }\n                                ReadoutView(readout: crossPost.createdReadout)\n                                if let scoreReadout = crossPost.scoreReadout(showColor: true) {\n                                    ReadoutView(readout: scoreReadout)\n                                }\n                                if let commentReadout = crossPost.commentReadout {\n                                    ReadoutView(readout: commentReadout)\n                                }\n                            }\n                            .contentShape(.rect)\n                            .onTapGesture {\n                                navigation.push(.post(crossPost))\n                            }\n                        }\n                    }\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                    .font(.footnote)\n                    .foregroundStyle(.themedSecondary)\n                }\n            }\n            .padding(.vertical, 8)\n            .background(.themedSecondaryGroupedBackground)\n            .contentShape(.rect(cornerRadius: Constants.main.standardSpacing))\n            .clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n            .contextMenu {\n                Button(\"Mark Read\", icon: .lemmy.markRead) {\n                    Task { await markAllAsRead(crossPosts) }\n                }\n            }\n            .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        }\n    }\n\n    func markAllAsRead(_ crossPosts: [Post]) async {\n        do {\n            try await post.api.markPostsAsRead(ids: Set(crossPosts.map(\\.id)))\n            ToastModel.main.add(.success(\"Read \\(crossPosts.count) posts\"))\n        } catch {\n            handleError(error)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/Feed Post Components/PostLinkHostView.swift",
    "content": "//\n//  PostLinkHostView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-26.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct PostLinkHostView: View {\n    let host: String\n    \n    var body: some View {\n        content\n            .lineLimit(1)\n            .imageScale(.small)\n            .foregroundStyle(.themedSecondary)\n    }\n    \n    var content: Text {\n        Text(Image(icon: .general.browser)) + Text(verbatim: \" \\(host)\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/Feed Post Components/PostTag.swift",
    "content": "//\n//  PostTag.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-23.\n//\n\nimport Foundation\nimport Icons\nimport SwiftUI\nimport Theming\n\nfunc postTag(active: Bool, icon: Icon, color: ThemedColor) -> Text {\n    if active {\n        Text(Image(icon: icon))\n            .foregroundStyle(color)\n    } else {\n        Text(verbatim: \"\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/FeedPostView.swift",
    "content": "//\n//  FeedPostView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-01-05.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\n/// View for rendering posts in feed\nstruct FeedPostView<EmbeddedContent: View>: View {\n    @Environment(AppState.self) private var appState: AppState\n    @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker?\n    @Environment(NavigationLayer.self) private var navigation\n    @Environment(FiltersTracker.self) var filtersTracker\n    @Environment(\\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor\n    @Environment(\\.communityContext) var communityContext\n    @Environment(\\.reportContext) var reportContext\n    \n    @State var obscured: Bool\n    \n    @Setting(\\.post_size) private var settingsPostSize\n    @Setting(\\.a11y_readPostIndicator) var readPostIndicator\n    @Setting(\\.a11y_readOutlineThickness) var readOutlineThickness\n    @Setting(\\.interactionBar_post) var postInteractionBar\n    @Setting(\\.interactionBar_postReport) var postReportInteractionBar\n    @Setting(\\.interactionBar_alternateReportLayout) var alternateInteractionBarLayoutForReports\n    \n    let post: Post\n    let favoredLink: PostViewNavigationLink?\n    let requireConsistentHeight: Bool\n    @State var overridePostSize: PostSize?\n    \n    var postSize: PostSize { overridePostSize ?? settingsPostSize }\n    \n    @ViewBuilder let embeddedContent: () -> EmbeddedContent\n    \n    init(\n        post: Post,\n        overridePostSize: PostSize? = nil,\n        favoredLink: PostViewNavigationLink? = nil,\n        requireConsistentHeight: Bool = false,\n        @ViewBuilder embeddedContent: @escaping () -> EmbeddedContent = { EmptyView() }\n    ) {\n        self.post = post\n        self.favoredLink = favoredLink\n        self.requireConsistentHeight = requireConsistentHeight\n        self.embeddedContent = embeddedContent\n        self._obscured = .init(wrappedValue: FiltersTracker.main.postWouldBeFiltered(post))\n        self._overridePostSize = .init(wrappedValue: overridePostSize)\n    }\n    \n    var body: some View {\n        Group {\n            if obscured {\n                obscuredContent\n                    .onTapGesture {\n                        withAnimation {\n                            obscured = false\n                        }\n                    }\n            } else {\n                content\n                    .overlay(alignment: .topLeading) {\n                        if differentiateWithoutColor, !(post.read.value ?? false), readPostIndicator == .outline {\n                            RoundedRectangle(cornerRadius: postSize.cornerRadius)\n                                .stroke(lineWidth: .init(readOutlineThickness))\n                                .foregroundStyle(.themedSecondary)\n                        }\n                    }\n                    .contentShape(.contextMenuPreview, .rect(cornerRadius: postSize.cornerRadius))\n                    .quickSwipes(post: post, configuration: interactionBarConfiguration)\n                    .contextMenu(post: post)\n            }\n        }\n        .contentShape(.interaction, .rect)\n        .paletteBorder(cornerRadius: postSize.cornerRadius)\n        .onChange(of: filtersTracker.changeHash) {\n            obscured = filtersTracker.postWouldBeFiltered(post)\n        }\n        .onAppear {\n            if shouldRenderCompact() {\n                overridePostSize = .compact\n            }\n        }\n        .onChange(of: settingsPostSize) {\n            if settingsPostSize == .tile {\n                overridePostSize = nil\n            } else if shouldRenderCompact() {\n                overridePostSize = .compact\n            }\n        }\n        .onChange(of: post.read.value) {\n            if shouldRenderCompact() {\n                withAnimation {\n                    overridePostSize = .compact\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var obscuredContent: some View {\n        Text(\"Hidden by filters\")\n            .italic()\n            .foregroundStyle(.themedSecondary)\n            .padding(Constants.main.standardSpacing)\n            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)\n            .background(.themedSecondaryGroupedBackground)\n            .clipShape(.rect(cornerRadius: postSize.cornerRadius))\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        switch postSize {\n        case .compact:\n            CompactPostView(post: post, requireConsistentHeight: requireConsistentHeight)\n        case .tile:\n            TilePostView(post: post)\n        case .headline:\n            HeadlinePostView(\n                post: post,\n                favoredLink: favoredLink,\n                requireConsistentHeight: requireConsistentHeight,\n                embeddedContent: embeddedContent\n            )\n        case .large:\n            LargePostView(post: post, favoredLink: favoredLink)\n        }\n    }\n    \n    var interactionBarConfiguration: PostBarConfiguration {\n        if reportContext != nil, alternateInteractionBarLayoutForReports {\n            return postReportInteractionBar\n        }\n        return postInteractionBar\n    }\n    \n    func shouldRenderCompact() -> Bool {\n        guard settingsPostSize != .tile, settingsPostSize != .compact else { return false }\n        return post.read.value ?? false &&\n            ((communityContext == nil && post.pinnedInstance) || (communityContext != nil && post.pinnedCommunity))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostBodyView.swift",
    "content": "//\n//  HeadlinePostBodyView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-17.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct HeadlinePostBodyView: View {\n    @Environment(\\.communityContext) var communityContext: Community?\n    \n    @Setting(\\.post_thumbnailLocation) var thumbnailLocation\n    @Setting(\\.safety_blurNsfw) var blurNsfw\n    \n    @ScaledMetric(relativeTo: .headline) var titleHostHeightLimit: CGFloat = 75\n\n    let post: Post\n    var requireConsistentHeight: Bool = false\n    \n    var blurred: Bool {\n        switch blurNsfw {\n        case .always: post.nsfw\n        case .outsideCommunity: post.nsfw && !(communityContext?.nsfw ?? false)\n        case .never: false\n        }\n    }\n    \n    var body: some View {\n        HStack(alignment: .top, spacing: Constants.main.standardSpacing) {\n            if thumbnailLocation == .left {\n                ThumbnailImageView(\n                    post: post,\n                    blurred: blurred,\n                    size: .standard,\n                    frame: .init(width: thumbnailSize, height: thumbnailSize)\n                )\n            }\n\n            if requireConsistentHeight {\n                titleAndHostView\n                    .frame(height: titleHostHeightLimit, alignment: .top)\n            } else {\n                titleAndHostView\n            }\n            \n            if thumbnailLocation == .right {\n                Spacer()\n                ThumbnailImageView(\n                    post: post,\n                    blurred: blurred,\n                    size: .standard,\n                    frame: .init(width: thumbnailSize, height: thumbnailSize)\n                )\n            }\n        }\n    }\n    \n    var thumbnailSize: CGFloat {\n        if requireConsistentHeight, titleHostHeightLimit < Constants.main.thumbnailSize * 1.5 {\n            titleHostHeightLimit\n        } else {\n            Constants.main.thumbnailSize\n        }\n    }\n    \n    @ViewBuilder\n    var titleAndHostView: some View {\n        VStack(alignment: .leading, spacing: Constants.main.halfSpacing) {\n            titleView\n            if let host = post.linkHost {\n                PostLinkHostView(host: host)\n                    .font(.subheadline)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var titleView: some View {\n        post.taggedTitle(communityContext: communityContext)\n            .symbolVariant(.fill)\n            .multilineTextAlignment(.leading)\n            .foregroundStyle((post.read.value ?? false) ? .themedSecondary : .themedPrimary)\n            .font(.headline)\n            .imageScale(.small)\n            .fixedSize(horizontal: false, vertical: true)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostView.swift",
    "content": "//\n//  HeadlinePostView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-19.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nstruct HeadlinePostView<EmbeddedContent: View>: View {\n    @Setting(\\.post_showCreator) var alwaysShowCreator\n    @Setting(\\.person_showAvatar) var showPersonAvatar\n    @Setting(\\.community_showAvatar) var showCommunityAvatar\n    @Setting(\\.a11y_readPostIndicator) var readPostIndicator\n    @Setting(\\.interactionBar_post) var postInteractionBar\n    @Setting(\\.interactionBar_postReport) var postReportInteractionBar\n    @Setting(\\.interactionBar_alternateReportLayout) var alternateInteractionBarLayoutForReports\n\n    @Environment(AppState.self) private var appState\n    @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker?\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.communityContext) var communityContext: Community?\n    @Environment(\\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor\n    @Environment(\\.reportContext) private var reportContext: Report?\n\n    let post: Post\n    let embeddedContent: EmbeddedContent\n    let favoredLink: PostViewNavigationLink?\n    let requireConsistentHeight: Bool\n\n    init(\n        post: Post,\n        favoredLink: PostViewNavigationLink? = nil,\n        requireConsistentHeight: Bool = false,\n        @ViewBuilder embeddedContent: () -> EmbeddedContent = { EmptyView() }\n    ) {\n        self.post = post\n        self.favoredLink = favoredLink\n        self.requireConsistentHeight = requireConsistentHeight\n        self.embeddedContent = embeddedContent()\n    }\n    \n    var topNavigationLink: PostViewNavigationLink {\n        if let favoredLink { return favoredLink }\n        return communityContext == nil ? .community : .creator\n    }\n    \n    var body: some View {\n        contentView\n            .background(.themedSecondaryGroupedBackground)\n            .environment(\\.postContext, post)\n    }\n    \n    var contentView: some View {\n        VStack(spacing: 0) {\n            VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n                HStack {\n                    switch topNavigationLink {\n                    case .community: communityLink\n                    case .creator: personLink\n                    }\n                    \n                    Spacer()\n                    \n                    if differentiateWithoutColor, readPostIndicator == .checkmark {\n                        ReadCheck(read: post.read)\n                    }\n                    \n                    if post.nsfw {\n                        Image(icon: .lemmy.nsfwTag)\n                            .foregroundStyle(.themedWarning)\n                    }\n                    \n                    PostEllipsisMenus(post: post)\n                }\n                \n                HeadlinePostBodyView(post: post, requireConsistentHeight: requireConsistentHeight)\n                \n                if alwaysShowCreator, communityContext == nil, topNavigationLink != .creator {\n                    personLink\n                }\n                \n                embeddedContent\n            }\n            .padding([.top, .horizontal], Constants.main.standardSpacing)\n            \n            InteractionBarView(\n                appState: appState,\n                post: post,\n                configuration: interactionBarConfiguration,\n                navigation: navigation,\n                commentTreeTracker: commentTreeTracker,\n                communityContext: communityContext,\n                reportContext: reportContext\n            )\n        }\n    }\n    \n    @ViewBuilder\n    var personLink: some View {\n        ExpectedView(post.creator) { creator in\n            FullyQualifiedLinkView(creator, labelStyle: .medium)\n        } placeholder: {\n            Text(verbatim: .personPlaceholder).redacted(reason: .placeholder)\n        }\n    }\n    \n    @ViewBuilder\n    var communityLink: some View {\n        ExpectedView(post.community) { community in\n            FullyQualifiedLinkView(community, labelStyle: .medium)\n        } placeholder: {\n            Text(verbatim: .communityPlaceholder).redacted(reason: .placeholder)\n        }\n    }\n    \n    var interactionBarConfiguration: PostBarConfiguration {\n        if reportContext != nil, alternateInteractionBarLayoutForReports {\n            return postReportInteractionBar\n        }\n        return postInteractionBar\n    }\n}\n\n// TODO: update mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment, .sizeThatFitsLayout) {\n//        HeadlinePostView(post: Post2.mock(.generic))\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/LargePostBodyView.swift",
    "content": "//\n//  LargePostBodyView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/07/2024.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct LargePostBodyView: View {\n    @Environment(\\.palette) var palette\n    @Environment(\\.communityContext) private var communityContext: Community?\n\n    let post: Post\n    let isPostPage: Bool\n    let shouldBlur: Bool\n\n    var body: some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            post.taggedTitle(communityContext: communityContext)\n                .foregroundStyle(\n                    (post.read.value ?? false && !isPostPage)\n                        ? .themedSecondary : .themedPrimary\n                )\n                .font(.headline)\n                .symbolVariant(.fill)\n                .imageScale(.small)\n\n            switch post.type {\n            case let .poll(poll):\n                PostPollView(post: post, poll: poll)\n                if post.content != nil {\n                    Divider().padding(.horizontal, -Constants.main.standardSpacing)\n                }\n            case let .media(url):\n                mediaView(url)\n            case let .embedded(url, originalLink):\n                VStack(spacing: Constants.main.standardSpacing) {\n                    mediaView(url)\n                    \n                    if isPostPage {\n                        OpenInLoopsButton(url: originalLink)\n                    }\n                }\n            case let .link(link):\n                WebsitePreviewView(link: link, shouldBlur: shouldBlur) {\n                    post.updateRead(true)\n                }\n            default:\n                EmptyView()\n            }\n            if let content = post.content {\n                if isPostPage {\n                    MarkdownWithLinkList(content, configuration: shouldBlur ? .defaultBlurred : .default)\n                } else {\n                    // Cut down on compute time for very long text posts by only rendering the first 4 blocks\n                    MarkdownText(\n                        Array([BlockNode](content).prefix(4)),\n                        configuration: .dimmed(palette: palette)\n                    )\n                    .lineLimit(post.linkUrl == nil ? 8 : 4)\n                }\n            }\n        }\n        .environment(\\.postContext, post)\n    }\n    \n    @ViewBuilder\n    func mediaView(_ url: URL) -> some View {\n        MediaView.largeImage(url: url, shouldBlur: shouldBlur) {\n            post.updateRead(true)\n        }\n        .frame(maxWidth: .infinity)\n    }\n    \n    // @Environment(\\.openURL) combined with the conditionally displayed url in .embedded causes significant lag\n    // due to openURL-based redraws, so we pull this into its own view to isolate openURL\n    private struct OpenInLoopsButton: View {\n        @Environment(\\.openURL) private var openURL\n        \n        let url: URL\n        \n        var body: some View {\n            Button(String(localized: loopsButtonText(originalLink: url))) {\n                openURL(url)\n            }\n            .buttonStyle(.bordered)\n        }\n        \n        func loopsButtonText(originalLink: URL) -> LocalizedStringResource {\n            if let host = originalLink.host() {\n                return \"View on \\(host)\"\n            } else {\n                return \"View on original host\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/LargePostView.swift",
    "content": "//\n//  LargePostView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-19.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct LargePostView: View {\n    @Setting(\\.post_showCreator) private var alwaysShowCreator\n    @Setting(\\.person_showAvatar) private var showPersonAvatar\n    @Setting(\\.community_showAvatar) private var showCommunityAvatar\n    @Setting(\\.safety_blurNsfw) var blurNsfw\n    @Setting(\\.a11y_readPostIndicator) var readPostIndicator\n    @Setting(\\.interactionBar_post) var postInteractionBar\n    @Setting(\\.interactionBar_postReport) var postReportInteractionBar\n    @Setting(\\.interactionBar_alternateReportLayout) var alternateInteractionBarLayoutForReports\n    \n    @Environment(AppState.self) private var appState\n    @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker?\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.communityContext) private var communityContext\n    @Environment(\\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor\n    @Environment(\\.reportContext) private var reportContext: Report?\n\n    let post: Post\n    let isPostPage: Bool\n    let favoredLink: PostViewNavigationLink?\n    \n    init(\n        post: Post,\n        isPostPage: Bool = false,\n        favoredLink: PostViewNavigationLink? = nil\n    ) {\n        self.post = post\n        self.isPostPage = isPostPage\n        self.favoredLink = favoredLink\n    }\n    \n    var shouldBlur: Bool {\n        switch blurNsfw {\n        case .always: post.nsfw\n        case .outsideCommunity: post.nsfw && !(communityContext?.nsfw ?? false)\n        case .never: false\n        }\n    }\n    \n    var topNavigationLink: PostViewNavigationLink {\n        if let favoredLink { return favoredLink }\n        return communityContext == nil || isPostPage ? .community : .creator\n    }\n    \n    var body: some View {\n        content\n            .background(.themedSecondaryGroupedBackground)\n            .environment(\\.postContext, post)\n    }\n    \n    var content: some View {\n        VStack(spacing: 0) {\n            VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n                HStack {\n                    switch topNavigationLink {\n                    case .community: communityLink\n                    case .creator: personLink\n                    }\n                    \n                    Spacer()\n                    \n                    if !isPostPage, differentiateWithoutColor, readPostIndicator == .checkmark {\n                        ReadCheck(read: post.read)\n                    }\n                    \n                    if post.nsfw {\n                        Image(icon: .lemmy.nsfwTag)\n                            .foregroundStyle(.themedWarning)\n                    }\n                    \n                    if !isPostPage {\n                        PostEllipsisMenus(post: post)\n                    }\n                }\n                \n                LargePostBodyView(post: post, isPostPage: isPostPage, shouldBlur: shouldBlur)\n                \n                if (alwaysShowCreator && communityContext == nil && topNavigationLink != .creator) || isPostPage {\n                    personLink\n                }\n                \n                if showDivider {\n                    Divider().padding(.horizontal, -Constants.main.standardSpacing)\n                }\n            }\n            .padding([.top, .horizontal], Constants.main.standardSpacing)\n            \n            InteractionBarView(\n                appState: appState,\n                post: post,\n                configuration: interactionBarConfiguration,\n                navigation: navigation,\n                commentTreeTracker: commentTreeTracker,\n                communityContext: communityContext,\n                reportContext: reportContext\n            )\n        }\n    }\n    \n    var interactionBarConfiguration: PostBarConfiguration {\n        if reportContext != nil, alternateInteractionBarLayoutForReports {\n            return postReportInteractionBar\n        }\n        return postInteractionBar\n    }\n    \n    var showDivider: Bool {\n        !(post.content?.isEmpty ?? true) || post.poll != nil\n    }\n    \n    @ViewBuilder\n    var personLink: some View {\n        ExpectedView(post.creator) { creator in\n            FullyQualifiedLinkView(creator, labelStyle: .medium)\n        } placeholder: {\n            Text(verbatim: .personPlaceholder).redacted(reason: .placeholder)\n        }\n    }\n    \n    @ViewBuilder\n    var communityLink: some View {\n        ExpectedView(post.community) { community in\n            FullyQualifiedLinkView(community, labelStyle: .medium)\n        } placeholder: {\n            Text(verbatim: .communityPlaceholder)\n                .redacted(reason: .placeholder)\n        }\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment, .sizeThatFitsLayout) {\n//        LargePostView(\n//            post: Post2.mock(.generic),\n//            isPostPage: true,\n//            favoredLink: nil\n//        )\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/PostPollView.swift",
    "content": "//\n//  PostPollView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-01-26.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct PostPollView: View {\n    @Environment(\\.hapticManager) var hapticManager\n    @Environment(\\.toastModel) var toastModel\n    @Environment(\\.colorScheme) var colorScheme\n\n    let post: Post\n    let poll: PostPoll\n\n    @State var resultsShownManually: Bool \n\n    @State var selected: Set<Int>\n\n    init(post: Post, poll: PostPoll) {\n        self.post = post\n        self.poll = poll\n        self._resultsShownManually = .init(initialValue: poll.hasVoted)\n        self._selected = .init(initialValue: .init(poll.choices.filter(\\.selected).map(\\.id)))\n    }\n\n    var body: some View {\n        VStack(alignment: .leading, spacing: 10) {\n            ForEach(Array(poll.choices.enumerated()), id: \\.offset) { _, choice in\n                choiceView(choice)\n            }\n            if !poll.hasEnded {\n                if selected.isEmpty, !poll.hasVoted {\n                    showResultsButtonView\n                } else {\n                    submitButtonView\n                }\n            }\n            footerView\n        }\n        .fixedSize(horizontal: false, vertical: true)\n        .animation(.snappy(duration: 0.2, extraBounce: 0.2), value: showResults)\n    }\n\n    @ViewBuilder\n    var showResultsButtonView: some View {\n        Button {\n            resultsShownManually.toggle()\n            hapticManager.play(haptic: .gentleInfo, tier: .low)\n        } label: {\n            Label(resultsShownManually ? \"Hide Results\" : \"Show Results\", icon: .lemmy.pollPost)\n                .foregroundStyle(.themedAccent)\n                .frame(maxWidth: .infinity, alignment: .leading)\n                .padding(.leading, 8)\n                .padding(.vertical, 8)\n                .background(.themedAccent.opacity(0.2), in: .rect(cornerRadius: 16))\n        }\n        .buttonStyle(.plain)\n    }\n\n    @ViewBuilder\n    var submitButtonView: some View {\n        Button {\n            if !self.poll.hasVoted {\n                self.resultsShownManually = true\n                self.post.voteInPoll(self.selected)\n                hapticManager.play(haptic: .gentleInfo, tier: .low)\n            }\n        } label: {\n            Label(\n                self.poll.hasVoted ? \"Submitted\" : \"Submit\",\n                icon: self.poll.hasVoted ? .general.success : .lemmy.send\n            )\n            .symbolVariant(.fill)\n            .foregroundStyle(self.poll.hasVoted ? .themedSecondary : .themedContrastingLabel)\n            .frame(maxWidth: .infinity, alignment: .leading)\n            .padding(.leading, 8)\n            .padding(.vertical, 8)\n            .background(\n                self.poll.hasVoted ? .themedTertiaryGroupedBackground : .themedAccent,\n                in: .rect(cornerRadius: 16)\n            )\n        }\n        .buttonStyle(.plain)\n    }\n\n    @ViewBuilder\n    var footerView: some View {\n        HStack {\n            if let endDate = poll.endDate {\n                Group {\n                    if poll.hasEnded {\n                        Text(\"Ended \\(endDate, format: .relative(presentation: .named, unitsStyle: .wide))\")\n                    } else {\n                        Text(\"Ends \\(endDate, format: .relative(presentation: .named, unitsStyle: .wide))\")\n                    }\n                }\n            }\n            Spacer()\n            Text(\"\\(poll.totalVotes) votes\")\n        }\n        .padding(.horizontal, 8)\n        .foregroundStyle(.themedSecondary)\n        .font(.footnote)\n    }\n\n    @ViewBuilder\n    func choiceView(_ choice: PostPollChoice) -> some View {\n        HStack(alignment: .top) {\n            if showCheckboxes {\n                let selected = self.selected.contains(choice.id)\n                Checkbox(isOn: selected)\n                    .opacity(!poll.hasVoted || selected ? 1 : 0)\n            }\n            VStack(alignment: .leading, spacing: 2) {\n                Text(choice.label)\n                    .padding(.vertical, 2)\n                resultsDetailsView(choice)\n            }\n        }\n        .multilineTextAlignment(.leading)\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .padding(.trailing, 16)\n        .padding(.leading, showCheckboxes ? 8 : 16)\n        .padding(.vertical, 8)\n        .background(.themedTertiaryGroupedBackground, in: .rect(cornerRadius: 16))\n        .onTapGesture {\n            if poll.hasEnded {\n                toastModel?.add(.basic(\"Poll has ended\", subtitle: \"This poll is no longer accepting votes.\", duration: 3))\n                return\n            }\n            if poll.hasVoted {\n                toastModel?.add(.basic(\"Already voted\", subtitle: \"You cannot change your vote.\", duration: 3))\n                return\n            }\n            hapticManager.play(haptic: .gentleInfo, tier: .low)\n            if poll.type == .single {\n                if selected == [choice.id] {\n                    selected = []\n                } else {\n                    selected = [choice.id]\n                }\n            } else {\n                if selected.contains(choice.id) {\n                    selected.remove(choice.id)\n                } else {\n                    selected.insert(choice.id)\n                }\n            }\n        }\n    }\n\n    @ViewBuilder\n    func resultsDetailsView(_ choice: PostPollChoice) -> some View {\n        HStack {\n            resultsBarView(choice)\n            HStack {\n                if showResults {\n                    Text(verbatim: \"\\(choice.percentage(poll: poll))%\")\n                        .foregroundStyle(.secondary)\n                } else {\n                    Text(verbatim: \"?\")\n                        .foregroundStyle(.tertiary)\n                }\n            }\n            .frame(width: showResults ? 35 : 15, alignment: .center)\n            .font(.footnote)\n        }\n    }\n\n    @ViewBuilder\n    func resultsBarView(_ choice: PostPollChoice) -> some View {\n        GeometryReader { proxy in\n            ZStack(alignment: .leading) {\n                Rectangle()\n                    .fill(colorScheme == .dark ? .themedSecondaryGroupedBackground : .themedTertiary.opacity(0.5))\n                let barWidth = proxy.size.width * CGFloat(choice.voteCount ?? 0) / CGFloat(max(1, poll.totalVotes))\n                // This creates a half-capsule\n                UnevenRoundedRectangle(\n                    topLeadingRadius: 0,\n                    bottomLeadingRadius: 0,\n                    bottomTrailingRadius: .greatestFiniteMagnitude,\n                    topTrailingRadius: .greatestFiniteMagnitude\n                )\n                .fill(.themedAccent)\n                .frame(width: showResults ? barWidth : 0)\n            }\n            .clipShape(.capsule)\n        }\n        .frame(height: 4)\n    }\n\n    var showCheckboxes: Bool {\n        !poll.hasEnded || poll.hasVoted\n    }\n\n    var showResults: Bool {\n        poll.hasEnded || resultsShownManually\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/TilePostView.swift",
    "content": "//\n//  TilePostView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-27.\n//\n\nimport Foundation\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport NukeUI\nimport SwiftUI\n\nstruct TilePostView: View {\n    @Setting(\\.a11y_readPostIndicator) var readPostIndicator\n    \n    @Environment(AppState.self) private var appState\n    @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker?\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.communityContext) var communityContext: Community?\n    @Environment(\\.parentFrameWidth) var parentFrameWidth: CGFloat\n    @Environment(\\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor\n    \n    let post: Post\n\n    // Note that these dimensions above sum to precisely the height of TileCommentView, though due to the grouping of title and community here, we get a bonus 10px for the content\n    // Total height in simplest form is:\n    // width + minTitleHeight + communityHeight + 17\n    @ScaledMetric(relativeTo: .footnote) var minTitleHeight: CGFloat = 36 // (2 * .footnote height), including built-in spacing\n    @ScaledMetric(relativeTo: .caption) var communityHeight: CGFloat = 16 // .caption height, including built-in spacing\n    \n    let contentHeightModifier: CGFloat = 10\n    // width cannot go below contentHeightModifier so contentWidth is never negative\n    var width: CGFloat { max(contentHeightModifier, (parentFrameWidth - (Constants.main.standardSpacing * 3)) / 2) }\n    var contentHeight: CGFloat { width - contentHeightModifier }\n    var frameHeight: CGFloat { width + minTitleHeight + communityHeight + 17 }\n    // Padding math\n    // Need to satisfy: padding + contentHeightModifier = 17\n    //\n    // Title : community spacing = 7\n    // Title + community external padding = (2 * Constants.main.standardSpacing) = 20\n    //\n    // Total padding = 27\n    // 27 + contentHeightModifier = 17\n    // contentHeightModifier = 10\n\n    var body: some View {\n        content\n            .frame(width: width, height: frameHeight)\n            .background(.themedSecondaryGroupedBackground)\n            .clipShape(.rect(cornerRadius: Constants.main.largeItemCornerRadius))\n            .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.largeItemCornerRadius))\n            .environment(\\.postContext, post)\n    }\n    \n    var content: some View {\n        VStack(alignment: .leading, spacing: 0) {\n            BaseImage(post: post, width: width, height: contentHeight)\n            \n            Divider()\n            \n            VStack(spacing: 7) {\n                titleSection\n                    .typesettingLanguage(.init(languageCode: .english))\n                \n                nameAndInfo\n            }\n            .padding(Constants.main.standardSpacing)\n        }\n    }\n    \n    @ViewBuilder\n    var titleSection: some View {\n        post.taggedTitle(communityContext: communityContext)\n            .symbolVariant(.fill)\n            .lineLimit(post.type.lineLimit)\n            .foregroundStyle(post.read.value ?? false ? .themedSecondary : .themedPrimary)\n            .font(.footnote)\n            .fontWeight(.semibold)\n            .frame(maxWidth: .infinity, minHeight: minTitleHeight, alignment: .topLeading)\n    }\n\n    var nameAndInfo: some View {\n        HStack(spacing: 6) {\n            Group {\n                if communityContext != nil {\n                    ExpectedView(post.creator) { creator in\n                        Text(creator.name)\n                    } placeholder: {\n                        Text(verbatim: .personPlaceholder).redacted(reason: .placeholder)\n                    }\n                } else {\n                    ExpectedView(post.community) { community in\n                        Text(community.name)\n                    } placeholder: {\n                        Text(verbatim: .communityPlaceholder).redacted(reason: .placeholder)\n                    }\n                }\n            }\n            .lineLimit(1)\n            .font(.caption)\n            .fontWeight(.semibold)\n            .foregroundStyle(.themedSecondary)\n            \n            Spacer()\n            \n            if differentiateWithoutColor, readPostIndicator == .checkmark {\n                ReadCheck(read: post.read, tiled: true)\n            }\n            \n            score\n        }\n        .frame(maxWidth: .infinity)\n    }\n    \n    var score: some View {\n        Menu {\n            ForEach(post.allMenuActions(\n                appState: appState,\n                showAllActions: false,\n                navigation: navigation,\n                commentTreeTracker: commentTreeTracker\n            ), id: \\.id) { action in\n                MenuButton(action: action)\n            }\n        } label: {\n            TileScoreView(post)\n        }\n        .onTapGesture {}\n        .popupAnchor()\n    }\n    \n    // MARK: - BaseImage\n    \n    struct BaseImage: View {\n        @Environment(\\.palette) var palette\n        @Environment(\\.communityContext) var communityContext\n        \n        @Setting(\\.safety_blurNsfw) var blurNsfw\n        \n        let post: Post\n        \n        let width: CGFloat\n        let height: CGFloat\n        \n        var blurred: Bool {\n            switch blurNsfw {\n            case .always: post.nsfw\n            case .outsideCommunity: post.nsfw && !(communityContext?.nsfw ?? false)\n            case .never: false\n            }\n        }\n        \n        var body: some View {\n            content\n                .overlay {\n                    if post.nsfw {\n                        Image(icon: .lemmy.nsfwTag)\n                            .symbolRenderingMode(.palette)\n                            .foregroundStyle(.themedBackground, .themedWarning)\n                            .imageScale(.small)\n                            .padding(.top, Constants.main.standardSpacing)\n                            .padding(.trailing, Constants.main.halfSpacing)\n                            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)\n                    }\n                }\n        }\n        \n        @ViewBuilder\n        var content: some View {\n            switch post.type {\n            case let .text(text):\n                MarkdownText(text, configuration: .caption(palette: palette))\n                    .foregroundStyle(.themedSecondary)\n                    .padding(Constants.main.standardSpacing)\n                    .frame(maxWidth: .infinity, maxHeight: height, alignment: .topLeading)\n                    .clipped()\n            case .titleOnly, .poll:\n                Image(icon: post.imageFallback.icon)\n                    .resizable()\n                    .scaledToFit()\n                    .foregroundStyle(.themedSecondary)\n                    .frame(width: Constants.main.thumbnailSize, height: Constants.main.thumbnailSize)\n                    .frame(maxWidth: .infinity, maxHeight: .infinity)\n            case .media, .embedded:\n                ThumbnailImageView(\n                    post: post,\n                    blurred: blurred,\n                    size: .tile,\n                    frame: .init(width: width, height: height)\n                )\n                .clipped()\n            case let .link(link):\n                ThumbnailImageView(\n                    post: post,\n                    blurred: blurred,\n                    size: .tile,\n                    frame: .init(width: width, height: height)\n                )\n                .aspectRatio(contentMode: .fill)\n                .clipped()\n                .overlay { linkHostOverlay(link) }\n            }\n        }\n        \n        func linkHostOverlay(_ link: PostLink) -> some View {\n            PostLinkHostView(host: link.host)\n                .font(.caption)\n                .padding(2)\n                .padding(.horizontal, 4)\n                .background {\n                    Capsule()\n                        .fill(.regularMaterial)\n                        .overlay(Capsule().fill(.themedBackground.opacity(0.25)))\n                }\n                .padding(Constants.main.compactSpacing)\n                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift",
    "content": "//\n//  FeedsView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-01-07.\n//\n\nimport Dependencies\nimport Foundation\nimport MlemBackend\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct FeedsView: View {\n    @Environment(AppState.self) var appState\n    @Environment(FiltersTracker.self) var filtersTracker\n    @Environment(BackendClient.self) var backendClient\n    \n    @Setting(\\.post_size) var postSize\n    @Setting(\\.feed_showRead) var showRead\n    @Setting(\\.tip_feedWelcomePrompt) var showWelcomePrompt\n    @Setting(\\.behavior_internetSpeed) var internetSpeed\n    @Setting(\\.links_embedLoops) var embedLoops\n    \n    @AppStorage(\"lastTestFlightUpdate\") var lastTestFlightUpdate: URL?\n\n    @ObservationIgnored @Dependency(\\.persistenceRepository) private var persistenceRepository\n    \n    @State var postFeedLoader: AggregatePostFeedLoader?\n    @State var scrollToTopTrigger: Bool = false\n    @State var initialListingType: ListingType?\n    @State var showHiddenReadBanner: Bool = false\n    @State var lastRefreshDate: Date?\n    \n    var feedOptions: [ListingType] {\n        ListingType.cases(for: appState.firstAccount.accountType, api: appState.firstApi)\n    }\n    \n    init(listingType: ListingType? = nil) {\n        _initialListingType = .init(initialValue: listingType)\n    }\n    \n    var body: some View {\n        content\n            .background(ThemedColor.themedGroupedBackground)\n            .themedGroupedBackground()\n            .scrollContentBackground(.hidden)\n            .modifier(\n                FeedSelectionTitleModifier(\n                    feedOptions: feedOptions,\n                    shouldScrollToTop: true,\n                    feedLoader: postFeedLoader,\n                    scrollToTopTrigger: $scrollToTopTrigger\n                )\n            )\n            .toolbar {\n                // SwiftUI complains if both this and the menu are in the same toolbar\n                if let postFeedLoader {\n                    FeedSortPicker(feedLoader: postFeedLoader, showTopTimescaleInIcon: true)\n                }\n            }\n            .conditionalNavigationTitle((postFeedLoader?.feedType.label ?? nil).map(String.init(localized:)) ?? \"\")\n            .navigationBarTitleDisplayMode(.inline)\n            .onChange(of: showRead) {\n                scrollToTopTrigger.toggle()\n                if showRead {\n                    showHiddenReadBanner = false\n                }\n                lastRefreshDate = nil\n            }\n            .onChange(of: appState.firstApi, initial: false) {\n                // ensure we always are showing an appropriate feed\n                if let postFeedLoader {\n                    Task {\n                        if !ListingType.cases(\n                            for: appState.firstAccount.accountType,\n                            api: appState.firstApi\n                        ).contains(postFeedLoader.feedType) {\n                            try await postFeedLoader.changeSortType(to: appState.initialFeedSortType)\n                            let newFeedSelection: ListingType =\n                                appState.firstAccount.accountType == .guest ? .all : .subscribed\n                            if newFeedSelection != postFeedLoader.feedType {\n                                await postFeedLoader.changeApi(to: appState.firstApi, context: filtersTracker.filterContext)\n                            }\n                            try await postFeedLoader.changeFeedType(to: newFeedSelection)\n                        }\n                    }\n                }\n            }\n            .task { await setupFeedLoader() }\n            .outdatedFeedPopup(feedLoader: postFeedLoader, onManualRefresh: {\n                guard !showRead else { return }\n                let now = Date()\n                if let lastRefresh = lastRefreshDate,\n                   now.timeIntervalSince(lastRefresh) < 5 {\n                    showHiddenReadBanner = true\n                }\n                lastRefreshDate = now\n            })\n            .environment(\\.feedContext, postFeedLoader?.feedType.feedContext)\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        ZStack {\n            if let postFeedLoader {\n                FancyScrollView(scrollToTopTrigger: $scrollToTopTrigger) {\n                    Section {\n                        if AccountsTracker.main.isEmpty, showWelcomePrompt, !appState.firstApi.willSendToken {\n                            FeedWelcomeView()\n                                .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n                        }\n                        if Bundle.main.isTestFlight,\n                           let testflightUrl = backendClient.testflightUpdate,\n                           lastTestFlightUpdate != testflightUrl {\n                            UpdateBannerView(url: testflightUrl)\n                                .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n                        }\n                        if showHiddenReadBanner, !showRead {\n                            HiddenReadBannerView {\n                                showHiddenReadBanner = false\n                            }\n                            .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n                        }\n                        PostGridView(postFeedLoader: postFeedLoader)\n                    } header: {\n                        Menu {\n                            FeedSelectionMenuView(\n                                feedOptions: feedOptions,\n                                shouldScrollToTop: false,\n                                feedLoader: postFeedLoader,\n                                scrollToTopTrigger: $scrollToTopTrigger\n                            )\n                        } label: {\n                            FeedHeaderView(feedDescription: postFeedLoader.feedType.description, dropdownStyle: .enabled(showBadge: false))\n                                .padding(.bottom, Constants.main.standardSpacing)\n                        }\n                        .buttonStyle(.plain)\n                    }\n                }\n                .animation(.snappy, value: backendClient.testflightUpdate != lastTestFlightUpdate)\n                .animation(.snappy, value: showHiddenReadBanner && !showRead)\n            } else {\n                ProgressView()\n                    .frame(maxWidth: .infinity, maxHeight: .infinity)\n                    .background(.themedGroupedBackground)\n            }\n        }\n    }\n    \n    @MainActor\n    func setupFeedLoader() async {\n        guard postFeedLoader == nil else { return }\n\n        @Setting(\\.behavior_internetSpeed) var internetSpeed\n        @Setting(\\.feed_showRead) var showReadPosts\n        @Setting(\\.feed_default) var defaultFeed\n        \n        var listingType: ListingType\n\n        do {\n            if let initialListingType {\n                listingType = initialListingType\n            } else if try await appState.firstApi.supports(.listingType(defaultFeed)) {\n                listingType = defaultFeed\n            } else {\n                listingType = .subscribed\n            }\n\n            // fallback to local if using guest account and selection requires authenticated account\n            if !(AppState.main.firstAccount is UserAccount),\n               !ListingType.guestCases.contains(listingType) {\n                listingType = .local\n            }\n\n            postFeedLoader = try await .init(\n                pageSize: internetSpeed.pageSize,\n                sortType: appState.initialFeedSortType,\n                showReadPosts: showReadPosts,\n                filterContext: filtersTracker.filterContext,\n                prefetchingConfiguration: .forPostSize(postSize),\n                urlCache: Constants.main.urlCache,\n                api: appState.firstApi,\n                feedType: listingType\n            )\n        } catch {\n            handleError(error)\n        }\n    }\n}\n\nprivate struct FeedSelectionTitleModifier: ViewModifier {\n    let feedOptions: [ListingType]\n    let shouldScrollToTop: Bool\n    var feedLoader: AggregatePostFeedLoader?\n    @Binding var scrollToTopTrigger: Bool\n    \n    @State var isAtTop: Bool = false\n    \n    func body(content: Content) -> some View {\n        content\n            .toolbar {\n                if !isAtTop, let feedLoader {\n                    ToolbarTitleMenu {\n                        FeedSelectionMenuView(\n                            feedOptions: feedOptions,\n                            shouldScrollToTop: shouldScrollToTop,\n                            feedLoader: feedLoader,\n                            scrollToTopTrigger: $scrollToTopTrigger\n                        )\n                    }\n                }\n            }\n            .isAtTopSubscriber(isAtTop: $isAtTop)\n    }\n}\n\nprivate struct FeedSelectionMenuView: View {\n    let feedOptions: [ListingType]\n    let shouldScrollToTop: Bool\n    @Binding var feedSelection: ListingType\n    @Binding var scrollToTopTrigger: Bool\n    \n    var body: some View {\n        ForEach(feedOptions, id: \\.self) { feed in\n            Button(\n                String(localized: feed.description.label),\n                icon: feed.description.icon\n            ) {\n                if shouldScrollToTop {\n                    scrollToTopTrigger.toggle()\n                    // delay feed switch to allow scroll to complete\n                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {\n                        feedSelection = feed\n                    }\n                } else {\n                    feedSelection = feed\n                }\n            }\n            .symbolVariant(feedSelection == feed ? .fill : .none)\n        }\n    }\n}\n\nextension FeedSelectionMenuView {\n    init(\n        feedOptions: [ListingType],\n        shouldScrollToTop: Bool,\n        feedLoader: AggregatePostFeedLoader,\n        scrollToTopTrigger: Binding<Bool>\n    ) {\n        self._feedSelection = .init(get: {\n            feedLoader.feedType\n        }, set: { newValue in\n            Task { @MainActor in\n                do {\n                    try await feedLoader.changeFeedType(to: newValue)\n                } catch {\n                    handleError(error)\n                }\n            }\n        })\n\n        self.feedOptions = feedOptions\n        self.shouldScrollToTop = shouldScrollToTop\n        self._scrollToTopTrigger = scrollToTopTrigger\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment(api: .realistic)) {\n//        FeedsView()\n//            .previewNavigationStack(backButtonLabel: \"Feeds\")\n//            .previewTabBar(selected: .feeds)\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/SectionIndexTitles.swift",
    "content": "//\n//  SectionIndexTitles.swift\n//  Mlem\n//\n//  Created by mormaer on 13/08/2023.\n//\n//\n\nimport Dependencies\nimport Haptics\nimport Icons\nimport SwiftUI\n\nstruct SectionIndexTitles: View {\n    @Environment(HapticManager.self) var hapticManager\n    \n    struct Section: Identifiable {\n        let label: String\n        var icon: Icon?\n        var id: String { label }\n    }\n    \n    let sections: [Section]\n    @Binding var sectionScroller: Int\n    \n    init(sections: [SubscriptionListSection], sectionScroller: Binding<Int>) {\n        self.sections = sections.map {\n            .init(label: $0.label, icon: $0.icon)\n        }\n        self._sectionScroller = sectionScroller\n    }\n    \n    @GestureState private var dragLocation: CGPoint = .zero\n\n    // Track which sidebar label we picked last so we\n    // only send a haptic when selecting a new one\n    @State var lastSelectedLabel: String = \"\"\n\n    var body: some View {\n        VStack {\n            ForEach(sections) { communitySection in\n                sectionTitle(for: communitySection)\n                    .frame(width: 12, height: 6)\n            }\n        }\n        .overlay {\n            GeometryReader { geo in\n                Color.clear\n                    .contentShape(.rect)\n                    .gesture(\n                        DragGesture(minimumDistance: 0, coordinateSpace: .local)\n                            .updating($dragLocation) { value, _, _ in\n                                // ignore if out of bounds--actually add a tiny bit of padding to the left side to make it feel right\n                                guard value.location.x > -20.0, value.location.y >= 0.0, value.location.y <= geo.size.height else {\n                                    return\n                                }\n                                \n                                // compute which section is currently dragged\n                                // height of one section is communitySections.count / geo.size.height\n                                // drag is thus (value.location.y / (communitySections.count / geo.size.height )) sections up\n                                // then do some algebra to make it prettier and round down to int\n                                let sectionIndex = min(\n                                    Int((value.location.y * Double(sections.count)) / geo.size.height),\n                                    sections.count - 1\n                                )\n                                \n                                let sectionLabel = sections[sectionIndex].label\n                                \n                                if sectionLabel != lastSelectedLabel {\n                                    Task { @MainActor in\n                                        lastSelectedLabel = sectionLabel\n                                        sectionScroller = sectionIndex\n                                        hapticManager.play(haptic: .rigidInfo, tier: .low)\n                                    }\n                                }\n                            }\n                    )\n            }\n        }\n    }\n}\n\n// Sidebar Label Views\n@ViewBuilder\nfunc sectionTitle(for section: SectionIndexTitles.Section) -> some View {\n    if let icon = section.icon {\n        SectionIndexImage(icon: icon)\n    } else {\n        SectionIndexText(label: section.label)\n    }\n}\n\nstruct SectionIndexText: View {\n    let label: String\n    var body: some View {\n        Text(label)\n            .font(.system(size: 11))\n            .fontWeight(.semibold)\n            .foregroundStyle(.themedPrimary)\n    }\n}\n\nstruct SectionIndexImage: View {\n    let icon: Icon\n    \n    var body: some View {\n        Image(icon: icon)\n            .resizable()\n            .frame(width: 8, height: 8)\n            .symbolVariant(.fill)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/SubscriptionListItemView.swift",
    "content": "//\n//  SubscriptionListItemView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 07/08/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct SubscriptionListItemView: View {\n    @Environment(\\.self) var environment\n    @Environment(AppState.self) private var appState\n    @Environment(NavigationLayer.self) private var navigation\n\n    @Setting(\\.subscriptions_sort) private var sort\n    @Setting(\\.subscriptions_instanceLocation) private var savedInstanceLocation\n\n    let community: Community\n    let section: SubscriptionListSection\n    let sectionIndicesShown: Bool\n    \n    var body: some View {\n        SubscriptionListNavigationButton(.community(community), label: label)\n            .contextMenu(community: community)\n            .swipeActions(edge: .trailing) {\n                Button(\"Unsubscribe\", icon: .lemmy.unsubscribe) {\n                    SubscribeAction(entity: community).execute(environment: environment)\n                }\n                .buttonStyle(.automatic)\n                .labelStyle(.iconOnly)\n                .tint(.red)\n            }\n            .padding(.trailing, sectionIndicesShown ? 5 : 0)\n    }\n    \n    @ViewBuilder\n    private func label() -> some View {\n        HStack(spacing: 15) {\n            switch instanceLocation(section: section) {\n            case .trailing:\n                CircleCroppedImageView(community, frame: 28)\n                (\n                    Text(community.name)\n                        + Text(verbatim: \"@\\(community.host)\")\n                        .foregroundStyle(.secondary)\n                        .font(.footnote)\n                )\n                .lineLimit(1)\n            case .bottom:\n                CircleCroppedImageView(community, frame: 36)\n                VStack(alignment: .leading, spacing: 0) {\n                    Text(community.name)\n                        .lineLimit(1)\n                    Text(verbatim: \"@\\(community.host)\")\n                        .foregroundStyle(.secondary)\n                        .font(.footnote)\n                }\n            case .disabled:\n                CircleCroppedImageView(community, frame: 28)\n                Text(community.name)\n                    .lineLimit(1)\n            }\n        }\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .contentShape(.rect)\n    }\n    \n    private func instanceLocation(section: SubscriptionListSection) -> InstanceLocation {\n        switch sort {\n        case .alphabetical:\n            savedInstanceLocation\n        case .instance:\n            section.label == String(localized: \"Other\") ? .trailing : .disabled\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/SubscriptionListNavigationButton.swift",
    "content": "//\n//  SubscriptionListNavigationButton.swift\n//  Mlem\n//\n//  Created by Sjmarf on 19/09/2024.\n//\n\nimport ComponentViews\nimport SwiftUI\n\nstruct SubscriptionListNavigationButton<Content: View>: View {\n    @Environment(NavigationLayer.self) var navigation\n    let destination: NavigationPage\n    @ViewBuilder var label: () -> Content\n    \n    init(_ destination: NavigationPage, @ViewBuilder label: @escaping () -> Content) {\n        self.destination = destination\n        self.label = label\n    }\n    \n    var body: some View {\n        MultiplatformView(phone: {\n            NavigationLink(destination, label: label)\n        }, pad: {\n            Button(action: {\n                navigation.path = []\n                navigation.root = destination\n            }, label: label)\n                .buttonStyle(.empty)\n        })\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/SubscriptionListView.swift",
    "content": "//\n//  SubscriptionListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/05/2024.\n//\n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\n@_spi(Advanced) import SwiftUIIntrospect\n\nstruct SubscriptionListView: View {\n    @Environment(AppState.self) private var appState\n    @Environment(NavigationLayer.self) private var navigation\n    @Environment(TabReselectTracker.self) var tabReselectTracker\n    \n    @Setting(\\.subscriptions_sort) private var sort\n    \n    @State var noDetail: Bool = false\n    \n    var feedOptions: [ListingType] {\n        ListingType.cases(for: appState.firstAccount.accountType, api: appState.firstApi)\n    }\n    \n    @State var sectionScroller: Int = 0\n    @State var errorDetails: ErrorDetails?\n    \n    @Weak var form: UICollectionView?\n    \n    var detailDisplayed: Bool {\n        if UIDevice.isPad {\n            noDetail ? false : navigation.path.isEmpty\n        } else {\n            navigation.path.isEmpty\n        }\n    }\n    \n    var subscriptions: SubscriptionList? {\n        (appState.firstSession as? UserSession)?.subscriptions\n    }\n    \n    var sectionIndicesShown: Bool {\n        !UIDevice.isPad && sort == .alphabetical && (subscriptions?.communities.count ?? 0) > 10\n    }\n    \n    var body: some View {\n        Group {\n            // TODO: iOS 18 deprecation remove compatibility shim\n            if #available(iOS 26, *) {\n                content\n                    .listSectionIndexVisibility(sectionIndicesShown ? .visible : .hidden)\n            } else {\n                content\n            }\n        }\n        .listStyle(.sidebar)\n        .navigationTitle(\"Feeds\")\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        let sections = subscriptions?.visibleSections(sort: sort) ?? []\n        \n        Form(tint: .themedPrimary) {\n            // TODO: iOS 18 deprecation remove compatibility shim\n            if #available(iOS 26, *) {\n                feeds\n                    .sectionIndexLabel(\"★\")\n            } else {\n                feeds\n            }\n            \n            if AccountsTracker.main.isEmpty {\n                Section {\n                    signedOutInfoView\n                        .listRowBackground(Color.clear)\n                }\n            }\n            if let errorDetails {\n                Section {\n                    ErrorView(errorDetails)\n                        .frame(maxWidth: .infinity)\n                        .listRowBackground(Color.clear)\n                }\n            } else {\n                ForEach(sections) { section in\n                    SubscriptionListSectionView(section: section, sectionIndicesShown: sectionIndicesShown)\n                        .id(section.label)\n                }\n                .scrollTargetLayout()\n            }\n        }\n        .introspect(.form, on: .iOS(.v17, .v18)) { introspectedForm in\n            form = introspectedForm\n        }\n        .onChange(of: sectionScroller) {\n            form?.scrollToItem(at: .init(row: 0, section: sectionScroller), at: .centeredVertically, animated: false)\n        }\n        .foregroundStyle(.themedPrimary)\n        .overlay(alignment: .trailing) {\n            if !UIDevice.isIos26, sectionIndicesShown {\n                SectionIndexTitles(\n                    sections: sections,\n                    sectionScroller: $sectionScroller\n                )\n            }\n        }\n        .toolbar {\n            if !(subscriptions?.communities.isEmpty ?? true) {\n                Menu(\"Sort\", icon: sort.icon) {\n                    ForEach(SubscriptionListSort.allCases, id: \\.self) { item in\n                        Toggle(\n                            item.label,\n                            icon: item.icon,\n                            isOn: .init(\n                                get: { sort == item },\n                                set: { _ in sort = item }\n                            )\n                        )\n                    }\n                }\n            }\n        }\n        .onChange(of: tabReselectTracker.flag) {\n            // normal reselect tracker does not work here thanks to NavigationSplitView, so we need to implement a custom one\n            if detailDisplayed, tabReselectTracker.flag {\n                tabReselectTracker.reset()\n                form?.scrollToItem(at: .init(row: 0, section: 0), at: .bottom, animated: true)\n            }\n        }\n        .onChange(of: (appState.firstSession as? UserSession)?.subscriptionListErrorDetails) {\n            if let details = (appState.firstSession as? UserSession)?.subscriptionListErrorDetails {\n                errorDetails = details\n            }\n        }\n        .scrollIndicators(sectionIndicesShown ? .hidden : .visible)\n        .refreshable {\n            do {\n                try await subscriptions?.refresh()\n                errorDetails = nil\n            } catch {\n                errorDetails = handleErrorWithDetails(error)\n            }\n        }\n        .background(.themedBackground)\n    }\n    \n    @ViewBuilder\n    var feeds: some View {\n        Section {\n            ForEach(feedOptions, id: \\.hashValue) { feedOption in\n                SubscriptionListNavigationButton(.feeds(feedOption)) {\n                    HStack(spacing: 15) {\n                        FeedIconView(\n                            feedDescription: feedOption.description,\n                            size: appState.firstSession is GuestSession ? 36 : 28\n                        )\n                        VStack(alignment: .leading) {\n                            Text(feedOption.description.label)\n                            if appState.firstSession is GuestSession {\n                                Text(feedOption.description.subtitle)\n                                    .font(.footnote)\n                                    .foregroundStyle(.themedSecondary)\n                            }\n                        }\n                    }\n                    .frame(maxWidth: .infinity, alignment: .leading)\n                    .contentShape(.rect)\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var signedOutInfoView: some View {\n        VStack {\n            Image(systemName: \"list.bullet\")\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .frame(height: 50)\n                .foregroundStyle(.themedTertiary)\n                .padding(.bottom, 5)\n            Text(\"Your subscriptions live here.\")\n                .font(.title2)\n                .fontWeight(.semibold)\n            Text(\"Log in or sign up to view your subscriptions.\")\n            HStack {\n                Button {\n                    navigation.openSheet(.logIn(.pickInstance))\n                } label: {\n                    Text(\"Log In\")\n                        .frame(minWidth: 80)\n                }\n                Button {\n                    navigation.openSheet(.signUp())\n                } label: {\n                    Text(\"Sign Up\")\n                        .frame(minWidth: 80)\n                }\n            }\n            .tint(.themedSecondary)\n            .buttonStyle(.bordered)\n        }\n        .multilineTextAlignment(.center)\n        .frame(maxWidth: .infinity)\n        .foregroundStyle(.themedSecondary)\n    }\n}\n\nprivate struct SubscriptionListSectionView: View {\n    let section: SubscriptionListSection\n    let sectionIndicesShown: Bool\n    \n    // TODO: iOS 18 deprecation remove compatibility shim\n    var body: some View {\n        if #available(iOS 26, *) {\n            content\n                .sectionIndexLabel(section.showInScroller ? section.label : nil)\n        } else {\n            content\n        }\n    }\n    \n    var content: some View {\n        Section(section.label) {\n            ForEach(section.communities) { (community: Community) in\n                SubscriptionListItemView(\n                    community: community,\n                    section: section,\n                    sectionIndicesShown: sectionIndicesShown\n                )\n            }\n        }\n    }\n}\n\nenum SubscriptionListSort: String, CaseIterable, Codable {\n    case alphabetical\n    case instance\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .alphabetical: \"Name\"\n        case .instance: \"Instance\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .alphabetical: .lemmy.alphabeticalSort\n        case .instance: .settings.qualifiedLabel\n        }\n    }\n}\n\nstruct SubscriptionListSection: Identifiable {\n    let label: String\n    var icon: Icon?\n    let communities: [Community]\n    let showInScroller: Bool\n    \n    init(label: String, icon: Icon? = nil, communities: [Community], showInScroller: Bool = true) {\n        self.label = label\n        self.icon = icon\n        self.communities = communities\n        self.showInScroller = showInScroller\n    }\n    \n    var id: String { label }\n}\n\nprivate extension SubscriptionList {\n    func visibleSections(sort: SubscriptionListSort) -> [SubscriptionListSection] {\n        var sections: [SubscriptionListSection] = .init()\n        if !favorites.isEmpty {\n            sections.append(.init(\n                label: String(localized: \"Favorites\"),\n                icon: .lemmy.favorited,\n                communities: favorites,\n                showInScroller: false))\n        }\n        switch sort {\n        case .alphabetical:\n            for section in alphabeticSections.sorted(by: { $0.key ?? \"~\" < $1.key ?? \"~\" }) {\n                sections.append(.init(label: section.key ?? \"#\", communities: section.value))\n            }\n        case .instance:\n            for section in instanceSections.sorted(by: { $0.key ?? \"~\" < $1.key ?? \"~\" }) {\n                sections.append(.init(label: section.key ?? String(localized: \"Other\"), communities: section.value))\n            }\n        }\n        \n        return sections\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Feeds/VisitAgainView.swift",
    "content": "//\n//  SavedFeedView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-01-07.\n//\n\nimport Dependencies\nimport Foundation\nimport MlemBackend\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct VisitAgainView: View {\n    @Environment(AppState.self) var appState\n    @Environment(FiltersTracker.self) var filtersTracker\n    @Environment(BackendClient.self) var backendClient\n    \n    let filter: GetContentFilter\n    \n    @State var mixedFeedLoader: DualSourceMixedFeedLoader\n    @State var postsFeedLoader: PostChildFeedLoader\n    @State var commentsFeedLoader: CommentChildFeedLoader\n\n    @State var selectedContentType: PersonContentType = .all\n    @State var scrollToTopTrigger: Bool = false\n    \n    init(filter: GetContentFilter) {\n        // need to grab some stuff from app storage to initialize with\n        @Setting(\\.behavior_internetSpeed) var internetSpeed\n        @Setting(\\.post_size) var postSize\n\n        let feedLoaders = DualSourceMixedFeedLoader.setup(\n            api: AppState.main.firstApi,\n            pageSize: internetSpeed.pageSize,\n            sortType: .new,\n            filter: filter\n        )\n        \n        self._mixedFeedLoader = .init(wrappedValue: feedLoaders.savedFeedLoader)\n        self._postsFeedLoader = .init(wrappedValue: feedLoaders.postFeedLoader)\n        self._commentsFeedLoader = .init(wrappedValue: feedLoaders.commentFeedLoader)\n        \n        self.filter = filter\n        \n    }\n    \n    var body: some View {\n        content\n            .themedGroupedBackground()\n            .scrollContentBackground(.hidden)\n            .conditionalNavigationTitle(filter.label)\n            .navigationBarTitleDisplayMode(.inline)\n            .outdatedFeedPopup(feedLoader: mixedFeedLoader)\n            .environment(\\.feedContext, .saved)\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        FancyScrollView(scrollToTopTrigger: $scrollToTopTrigger) {\n            BubblePicker(PersonContentType.allCases, selected: $selectedContentType, label: \\.label)\n            PersonContentGridView(feedLoader: .standard(selectedFeedLoader, contentType: selectedContentType))\n        }\n    }\n    \n    var selectedFeedLoader: StandardFeedLoader<PersonContent> {\n        switch selectedContentType {\n        case .all: mixedFeedLoader\n        case .posts: postsFeedLoader\n        case .comments: commentsFeedLoader\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Inbox/InboxView+Types.swift",
    "content": "//\n//  InboxView+Types.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-16.\n//\n\nimport Icons\nimport SwiftUI\nimport Theming\n\nextension InboxView {\n    enum Feed: CaseIterable, Identifiable {\n        case inbox, modMail\n        \n        var id: Feed { self }\n        \n        var label: LocalizedStringResource {\n            switch self {\n            case .inbox: \"Inbox\"\n            case .modMail: \"Mod Mail\"\n            }\n        }\n        \n        func subtitle(isAdmin: Bool) -> LocalizedStringResource {\n            switch self {\n            case .inbox:\n                return \"Replies, mentions and messages\"\n            case .modMail:\n                if isAdmin {\n                    return \"Reports and Registration Applications\"\n                } else {\n                    return \"Reports from communities you moderate\"\n                }\n            }\n        }\n        \n        var icon: Icon {\n            switch self {\n            case .inbox: .lemmy.inbox\n            case .modMail: .lemmy.moderation\n            }\n        }\n        \n        var color: ThemedColor {\n            switch self {\n            case .inbox: .themedInbox\n            case .modMail: .themedModeration\n            }\n        }\n    }\n    \n    enum Tab: CaseIterable, Identifiable {\n        case all, replies, mentions, messages\n        \n        var id: Tab { self }\n        \n        var label: LocalizedStringResource {\n            switch self {\n            case .all: \"All\"\n            case .replies: \"Replies\"\n            case .mentions: \"Mentions\"\n            case .messages: \"Messages\"\n            }\n        }\n    }\n    \n    enum ModTab: CaseIterable, Identifiable {\n        case reports, applications\n        \n        var id: ModTab { self }\n        \n        var label: LocalizedStringResource {\n            switch self {\n            case .reports: \"Reports\"\n            case .applications: \"Applications\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Inbox/InboxView+Views.swift",
    "content": "//\n//  InboxView+Views.swift\n//  Mlem\n//\n//  Created by Sjmarf on 22/09/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension InboxView {\n    @ViewBuilder\n    var inboxFeedView: some View {\n        LazyVStack(spacing: 0, pinnedViews: UIDevice.isIos26 ? [] : [.sectionHeaders]) {\n            Section {\n                ForEach(feedLoader.items, id: \\.inboxId) { notification in\n                    Group {\n                        switch notification.content {\n                        case let .message(message):\n                            MessageView(message: message, notification: notification)\n                        case let .reply(comment), let .mention(comment):\n                            ReplyView(notification: notification, comment: comment)\n                        }\n                    }\n                    .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n                    .onAppear {\n                        do {\n                            try inboxFeedLoader.loadIfThreshold(notification)\n                        } catch {\n                            handleError(error)\n                        }\n                    }\n                }\n                \n                EndOfFeedView(feedLoader: feedLoader, viewType: .cartoon)\n            } header: {\n                if appState.firstApi.supports(.viewMentionsAndPrivateMessages, defaultValue: false) {\n                    sectionHeader\n                }\n            }\n        }\n        .animation(.easeOut(duration: 0.1), value: feedLoader.items.isEmpty)\n        .padding(\n            .top,\n            appState.firstApi.supports(.viewMentionsAndPrivateMessages, defaultValue: false) ? 0 : Constants.main.standardSpacing\n        )\n    }\n    \n    @ViewBuilder\n    var modMailFeedView: some View {\n        LazyVStack(spacing: 0) {\n            if appState.firstApi.isAdmin {\n                BubblePicker(\n                    ModTab.allCases,\n                    selected: $selectedModTab,\n                    label: \\.label,\n                    value: { tab in\n                        if let unreadCount = (appState.firstSession as? UserSession)?.unreadCount {\n                            switch tab {\n                            case .reports:\n                                return unreadCount.reportTotal\n                            case .applications:\n                                return unreadCount.registrationApplications\n                            }\n                        }\n                        return 0\n                    }\n                )\n            }\n            ForEach(currentModFeedLoader.items, id: \\.inboxId) { item in\n                Group {\n                    switch item {\n                    case let .application(application):\n                        RegistrationApplicationView(application: application)\n                    case let .report(report):\n                        ReportView(report: report)\n                    }\n                }\n                .padding([.horizontal, .bottom], Constants.main.standardSpacing)\n                .onAppear {\n                    do {\n                        try currentModFeedLoader.loadIfThreshold(item)\n                    } catch {\n                        handleError(error)\n                    }\n                }\n            }\n            EndOfFeedView(feedLoader: currentModFeedLoader, viewType: .cartoon)\n        }\n        .padding(.top, Constants.main.standardSpacing)\n        .animation(.easeOut(duration: 0.1), value: currentModFeedLoader.items.isEmpty)\n    }\n    \n    @ViewBuilder\n    var sectionHeader: some View {\n        BubblePicker(\n            Tab.allCases,\n            selected: $selectedTab,\n            label: \\.label,\n            value: { tab in\n                if let unreadCount = (appState.firstSession as? UserSession)?.unreadCount {\n                    switch tab {\n                    case .all:\n                        return unreadCount.personalTotal\n                    case .replies:\n                        return unreadCount.replies\n                    case .mentions:\n                        return unreadCount.mentions\n                    case .messages:\n                        return unreadCount.messages\n                    }\n                }\n                return 0\n            }\n        )\n        .background(.themedGroupedBackground.opacity(headerPinned ? 1 : 0))\n        .background(.bar)\n    }\n    \n    @ToolbarContentBuilder\n    var toolbar: some ToolbarContent {\n        ToolbarItem(placement: .topBarLeading) {\n            if #available(iOS 26, *) {\n                if showRead {\n                    hideReadButton\n                } else {\n                    hideReadButton\n                        .buttonStyle(.glassProminent)\n                }\n            } else {\n                hideReadButton\n            }\n        }\n        if selectedFeed == .inbox {\n            MarkAllAsReadButton()\n        }\n    }\n    \n    @ViewBuilder\n    var hideReadButton: some View {\n        Button {\n            showRead.toggle()\n            let message: LocalizedStringResource = showRead ? \"Showing Read\" : \"Hiding Read\"\n            toastModel.add(.success(message))\n        } label: {\n            Label(\"Hide Read\", icon: .general.filterMenu)\n                .symbolVariant(showRead ? .none : .fill)\n        }\n    }\n    \n    @ViewBuilder\n    var headerView: some View {\n        let availableFeeds = availableFeeds\n        Menu {\n            if availableFeeds.count > 1 {\n                Picker(\"Feed\", selection: $selectedFeed) {\n                    ForEach(availableFeeds) { feedType in\n                        Label(String(localized: feedType.label), icon: feedType.icon)\n                            .tag(feedType)\n                    }\n                }\n            }\n        } label: {\n            FeedHeaderView(\n                feedDescription: .init(\n                    label: selectedFeed.label,\n                    subtitle: selectedFeed.subtitle(isAdmin: appState.firstApi.isAdmin),\n                    color: selectedFeed.color,\n                    icon: selectedFeed.icon,\n                    iconScaleFactor: 0.5\n                ),\n                dropdownStyle: availableFeeds.count > 1 ? .enabled(showBadge: showBadge) : .disabled\n            )\n        }\n    }\n    \n    @ViewBuilder\n    var signedOutInfoView: some View {\n        VStack {\n            Image(icon: .lemmy.inbox)\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .frame(height: 80)\n                .foregroundStyle(.themedAccent)\n            Text(AccountsTracker.main.isEmpty ? \"Log in or sign up to view your inbox.\" : \"Switch account to view your inbox.\")\n                .font(.title2)\n                .padding(.horizontal)\n                .fontWeight(.semibold)\n                .padding(.bottom, Constants.main.halfSpacing)\n            if AccountsTracker.main.isEmpty {\n                HStack {\n                    infoViewButton(\"Log In\") {\n                        navigation.openSheet(.logIn(.pickInstance))\n                    }\n                    infoViewButton(\"Sign Up\") {\n                        navigation.openSheet(.signUp())\n                    }\n                }\n            } else {\n                infoViewButton(\"Switch Account\") {\n                    navigation.openSheet(.quickSwitcher)\n                }\n            }\n        }\n        .buttonStyle(.borderedProminent)\n        .multilineTextAlignment(.center)\n        .frame(maxWidth: .infinity)\n        .padding(.horizontal)\n    }\n    \n    @ViewBuilder\n    private func infoViewButton(_ title: LocalizedStringResource, callback: @escaping () -> Void) -> some View {\n        Button(action: callback) {\n            Text(title)\n                .padding(.vertical, 4)\n                .padding(.horizontal, 8)\n                .frame(minWidth: 100)\n        }\n    }\n    \n    var showBadge: Bool {\n        guard let unreadCount = (appState.firstSession as? UserSession)?.unreadCount else { return false }\n        switch selectedFeed {\n        case .inbox: return unreadCount.moderationTotal > 0\n        case .modMail: return unreadCount.personalTotal > 0\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Inbox/InboxView.swift",
    "content": "//\n//  InboxView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 19/05/2024.\n//\n\nimport Haptics\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct InboxView: View {\n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(FiltersTracker.self) var filtersTracker\n    @Environment(ToastModel.self) var toastModel\n    \n    @Setting(\\.inbox_showRead) var showRead\n    \n    @State var headerPinned: Bool = false\n    @State var selectedFeed: Feed = .inbox\n    @State var selectedTab: Tab = .all\n    @State var selectedModTab: ModTab = .reports\n    \n    @State var applications: [RegistrationApplication]?\n    @State var reports: [Report]?\n    \n    @State var replyFeedLoader: ReplyChildFeedLoader\n    @State var mentionFeedLoader: MentionChildFeedLoader\n    @State var messageFeedLoader: MessageChildFeedLoader\n    @State var inboxFeedLoader: InboxFeedLoader\n    \n    @State var reportFeedLoader: ReportChildFeedLoader\n    @State var applicationFeedLoader: ApplicationChildFeedLoader\n    @State var modMailFeedLoader: ModMailFeedLoader\n    \n    @State var showRefreshPopup: Bool = false\n    \n    init() {\n        @Setting(\\.behavior_internetSpeed) var internetSpeed\n        @Setting(\\.inbox_showRead) var showRead\n        \n        let inboxFeedLoaders = InboxFeedLoader.setup(\n            api: AppState.main.firstApi,\n            pageSize: internetSpeed.pageSize,\n            sortType: .new,\n            showRead: showRead\n        )\n        \n        self._replyFeedLoader = .init(wrappedValue: inboxFeedLoaders.replyFeedLoader)\n        self._mentionFeedLoader = .init(wrappedValue: inboxFeedLoaders.mentionFeedLoader)\n        self._messageFeedLoader = .init(wrappedValue: inboxFeedLoaders.messageFeedLoader)\n        self._inboxFeedLoader = .init(wrappedValue: inboxFeedLoaders.inboxFeedLoader)\n        \n        let modMailFeedLoaders = ModMailFeedLoader.setup(\n            api: AppState.main.firstApi,\n            pageSize: internetSpeed.pageSize,\n            sortType: .new,\n            showRead: showRead\n        )\n        \n        self._reportFeedLoader = .init(wrappedValue: modMailFeedLoaders.reportFeedLoader)\n        self._applicationFeedLoader = .init(wrappedValue: modMailFeedLoaders.applicationFeedLoader)\n        self._modMailFeedLoader = .init(wrappedValue: modMailFeedLoaders.modMailFeedLoader)\n    }\n    \n    var feedLoader: StandardFeedLoader<InboxNotification> {\n        if appState.firstApi.supports(.viewMentionsAndPrivateMessages, defaultValue: false) {\n            switch selectedTab {\n            case .all:\n                inboxFeedLoader\n            case .replies:\n                replyFeedLoader\n            case .mentions:\n                mentionFeedLoader\n            case .messages:\n                messageFeedLoader\n            }\n        } else {\n            replyFeedLoader\n        }\n    }\n    \n    var currentModFeedLoader: StandardFeedLoader<ModMailItem> {\n        switch selectedModTab {\n        case .applications: applicationFeedLoader\n        case .reports: reportFeedLoader\n        }\n    }\n    \n    var availableFeeds: [Feed] {\n        if appState.isModOrAdmin, appState.firstApi.supports(.viewReports, defaultValue: false) {\n            return [.inbox, .modMail]\n        }\n        return [.inbox]\n    }\n    \n    var body: some View {\n        if appState.firstSession is GuestSession {\n            signedOutInfoView\n        } else {\n            content\n                .themedGroupedBackground()\n                .navigationBarTitleDisplayMode(.inline)\n                .toolbar { toolbar }\n                .loadFeed(inboxFeedLoader)\n                .loadFeed(modMailFeedLoader, shouldLoad: appState.firstApi.supports(.viewReports, defaultValue: false))\n                .onChange(of: appState.firstApi, initial: false) {\n                    if appState.firstAccount is UserAccount {\n                        Task {\n                            await inboxFeedLoader.changeApi(to: appState.firstApi, context: filtersTracker.filterContext)\n                            await modMailFeedLoader.changeApi(to: appState.firstApi, context: filtersTracker.filterContext)\n                        }\n                        showRefreshPopup = true\n                    }\n                }\n                .onChange(of: showRead, initial: false) {\n                    Task {\n                        do {\n                            if showRead {\n                                try await inboxFeedLoader.showRead()\n                                if appState.firstApi.supports(.viewReports, defaultValue: false) {\n                                    try await modMailFeedLoader.showRead()\n                                }\n                            } else {\n                                try await inboxFeedLoader.hideRead()\n                                if appState.firstApi.supports(.viewReports, defaultValue: false) {\n                                    try await modMailFeedLoader.hideRead()\n                                }\n                            }\n                        } catch {\n                            handleError(error)\n                        }\n                    }\n                }\n                .refreshable {\n                    _ = await Task {\n                        await refresh()\n                    }.result\n                }\n                .onChange(of: (appState.firstSession as? UserSession)?.unreadCount?.refreshNumber ?? 0) { oldValue, newValue in\n                    // The newValue > oldValue check stops the popup from appearing when the user switches accounts.\n                    // This is a little janky, but it works\n                    if newValue > oldValue, feedLoader.loadingState != .loading {\n                        showRefreshPopup = true\n                    }\n                }\n                .overlay(alignment: .bottom) {\n                    RefreshPopupView(\"Inbox is outdated\", isPresented: $showRefreshPopup) {\n                        Task { @MainActor in\n                            await refresh()\n                        }\n                    }\n                }\n        }\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        FancyScrollView(reselectAction: toggleFeed) {\n            VStack(spacing: 0) {\n                headerView\n                GeometryReader { geo in\n                    Color.red.preference(\n                        key: ScrollOffsetKey.self,\n                        value: geo.frame(in: .named(\"inboxScrollView\")).origin.y >= 0\n                    )\n                }\n                .frame(width: 0, height: 0)\n                .onPreferenceChange(ScrollOffsetKey.self, perform: { value in\n                    if value != headerPinned {\n                        if UIDevice.isIos26, headerPinned { return }\n                        headerPinned = value\n                    }\n                })\n                switch selectedFeed {\n                case .inbox:\n                    inboxFeedView\n                case .modMail:\n                    modMailFeedView\n                }\n            }\n        }\n        .coordinateSpace(name: \"inboxScrollView\")\n    }\n    \n    private func refresh() async {\n        do {\n            if selectedFeed == .modMail, !appState.isModOrAdmin {\n                selectedFeed = .inbox\n            }\n            switch selectedFeed {\n            case .inbox:\n                try await inboxFeedLoader.refresh(clearBeforeRefresh: false)\n                if appState.firstApi.supports(.viewReports, defaultValue: false) {\n                    Task {\n                        try await modMailFeedLoader.refresh(clearBeforeRefresh: false)\n                    }\n                }\n            case .modMail:\n                try await modMailFeedLoader.refresh(clearBeforeRefresh: false)\n                Task {\n                    try await inboxFeedLoader.refresh(clearBeforeRefresh: false)\n                }\n            }\n        } catch {\n            handleError(error)\n        }\n    }\n    \n    private func toggleFeed() {\n        selectedFeed = selectedFeed == .inbox && appState.isModOrAdmin ? .modMail : .inbox\n    }\n}\n\nprivate struct ScrollOffsetKey: PreferenceKey {\n    typealias Value = Bool\n    static var defaultValue = false\n    static func reduce(value: inout Value, nextValue: () -> Value) {}\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Inbox/MarkAllAsReadButton.swift",
    "content": "//\n//  MarkAllAsReadButton.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-15.\n//\n\nimport Haptics\nimport SwiftUI\n\nstruct MarkAllAsReadButton: ToolbarContent {\n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n\n    @State var animationPlaying: Bool = false\n    @State var phaseAnimatorTrigger: Bool = false\n    \n    var body: some ToolbarContent {\n        Group {\n            if newMessagesExist || animationPlaying || !UIDevice.isIos26 {\n                ToolbarItem(placement: .topBarTrailing) {\n                    PhaseAnimator([0, 1], trigger: phaseAnimatorTrigger) { value in\n                        Button {\n                            hapticManager.play(haptic: .gentleInfo, tier: .low)\n                            animationPlaying = true\n                            phaseAnimatorTrigger.toggle()\n                            Task {\n                                do {\n                                    try await appState.firstApi.markAllAsRead()\n                                    try await Task.sleep(for: .seconds(0.25))\n                                } catch {\n                                    handleError(error)\n                                }\n                                animationPlaying = false\n                            }\n                        } label: {\n                            if UIDevice.isIos26 {\n                                label(value: value)\n                            } else {\n                                label(value: value)\n                                    .background(.bar, in: .capsule)\n                            }\n                        }\n                        .opacity((newMessagesExist || value != 0) ? 1 : 0)\n                    }\n                }\n            }\n        }\n    }\n    \n    var newMessagesExist: Bool {\n        !animationPlaying && ((appState.firstSession as? UserSession)?.unreadCount?.personalTotal ?? 0) != 0\n    }\n    \n    @ViewBuilder\n    func label(value: Int) -> some View {\n        HStack {\n            Image(icon: .lemmy.markRead)\n                .imageScale(.small)\n            Text(\"All\")\n        }\n        .opacity((value == 0 && newMessagesExist) ? 1 : 0)\n        .overlay {\n            if value != 0 {\n                Image(icon: .general.success)\n                    .imageScale(.small)\n                    .fontWeight(.semibold)\n            }\n        }\n        .fixedSize()\n        .padding(.vertical, 2)\n        .padding(.horizontal, 10)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Profile/Profile View.swift",
    "content": "//\n//  Profile Tab View.swift\n//  Mlem\n//\n//  Created by Jake Shirley on 6/26/23.\n//\n\nimport Dependencies\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ProfileView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    var body: some View {\n        if let person = appState.firstPerson {\n            PersonView(person: person, isProfileTab: true, visitContext: nil)\n                .toolbar {\n                    if person.api.supports(.editProfile, defaultValue: false) {\n                        ToolbarItem(placement: .secondaryAction) {\n                            Button(\"Edit\", icon: .general.edit) {\n                                navigation.openSheet(.settings(.profile))\n                            }\n                        }\n                    }\n                }\n                .id(person.actorId)\n        } else if let instance = appState.firstSession.instance {\n            InstanceView(instance: instance, visitContext: nil)\n                .id(instance.actorId)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AboutMlemView.swift",
    "content": "//\n//  AboutMlemView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2024.\n//\n\nimport ComponentViews\nimport Haptics\nimport SwiftUI\nimport Theming\n\nstruct AboutMlemView: View {\n    @Environment(\\.palette) var palette\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(ToastModel.self) var toastModel\n    \n    var body: some View {\n        Form {\n            Section {} header: {\n                appHeaderView\n                    .listRowBackground(palette.groupedBackground.primary)\n                    .foregroundStyle(.themedPrimary)\n            }\n            .textCase(nil)\n            .listRowInsets(.init(top: 50, leading: 0, bottom: 15, trailing: 0))\n            Section {\n                Link(destination: URL(string: \"https://mlem.group\")!) {\n                    FormChevron { Label(\"Website\", icon: .general.website) }\n                        .foregroundStyle(.themedPrimary)\n                }\n                .gradientTint(.themedColorfulAccent(2))\n                Link(destination: URL(string: \"https://lemmy.ml/c/mlemapp\")!) {\n                    FormChevron { Label(\"Lemmy Community\", icon: .lemmy.community) }\n                        .foregroundStyle(.themedPrimary)\n                }\n                .gradientTint(.themedColorfulAccent(3))\n                Link(destination: URL(string: \"https://matrix.to/#/#mlemappspace:matrix.org\")!) {\n                    FormChevron { Label(\"Matrix Room\", image: \"matrix.logo\") }\n                        .foregroundStyle(.themedPrimary)\n                }\n                .tint(Color.black.gradient) // not ThemedColor because white tint turns this into white square\n                Link(destination: URL(string: \"https://github.com/mlemgroup/mlem\")!) {\n                    FormChevron { Label(\"GitHub Repository\", image: \"github.logo\") }\n                        .foregroundStyle(.themedPrimary)\n                }\n                .tint(Color.black.gradient) // not ThemedColor because white tint turns this into white square\n            }\n            Section {\n                NavigationLink(\"Privacy Policy\", icon: .settings.privacy, destination: .settings(.document(.privacyPolicy)))\n                    .gradientTint(.themedColorfulAccent(2))\n                NavigationLink(\"EULA\", icon: .settings.eula, destination: .settings(.document(.eula)))\n                    .gradientTint(.themedColorfulAccent(0))\n                NavigationLink(\"Licenses\", icon: .settings.licence, destination: .settings(.licences))\n                    .gradientTint(.themedColorfulAccent(4))\n            }\n        }\n        .buttonStyle(.plain)\n        .labelStyle(.squircle)\n        .navigationTitle(\"About Mlem\")\n    }\n    \n    @ViewBuilder\n    func linkView(_ title: LocalizedStringResource, systemImage: String, destination: String) -> some View {\n        Link(destination: URL(string: destination)!) {\n            HStack {\n                Spacer()\n                Image(icon: .general.forward)\n                    .imageScale(.small)\n                    .foregroundStyle(.themedTertiary)\n            }\n            .contentShape(.rect)\n        }\n    }\n    \n    @ViewBuilder\n    var appHeaderView: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            Image(\"logo\")\n                .resizable()\n                .frame(width: 120, height: 120)\n                .clipShape(.circle)\n            \n            Button {\n                let pasteboard = UIPasteboard.general\n                pasteboard.string = \"Mlem \\(versionString)\"\n                hapticManager.play(haptic: .lightSuccess, tier: .low)\n                toastModel.add(.success(\"Copied\"))\n            } label: {\n                HStack {\n                    Text(String(\"Mlem \\(versionString)\"))\n                    Image(icon: .general.copy)\n                        .symbolVariant(.fill)\n                        .imageScale(.small)\n                }\n            }\n            .foregroundStyle(.themedSecondary)\n            .buttonStyle(.empty)\n            .frame(maxWidth: .infinity)\n        }\n        .frame(maxWidth: .infinity)\n    }\n    \n    var versionString: String {\n        var result = \"n/a\"\n\n        if let releaseVersion = Bundle.main.releaseVersionNumber {\n            result = releaseVersion\n        }\n\n        if let buildVersion = Bundle.main.buildVersionNumber {\n            result.append(\" (\\(buildVersion))\")\n        }\n\n        return result\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AccessibilitySettingsView.swift",
    "content": "//\n//  AccessibilitySettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-19.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct AccessibilitySettingsView: View {\n    @Environment(\\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor: Bool\n\n    @Setting(\\.a11y_readPostIndicator) var readPostIndicator\n    @Setting(\\.a11y_websiteThumbnailIcon) var websiteThumbnailIcon\n    @Setting(\\.a11y_showSettingsIcons) var showSettingsIcons\n    @Setting(\\.a11y_zoomSliderLocation) var zoomSliderLocation\n    @Setting(\\.media_animatedAvatars) var animatedAvatars\n    @Setting(\\.a11y_showInteractionBarButtonBackground) var showInteractionBarButtonBackground\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Accessibility\",\n                description: \"Customize Mlem to work best for you. Some features are tied to system-wide accessibility settings.\",\n                icon: .settings.accessibility\n            )\n            .gradientTint(.themedColorfulAccent(2))\n            if differentiateWithoutColor {\n                Section {\n                    NavigationLink(\n                        \"Post Read Indicator\",\n                        value: .init(localized: readPostIndicator.label),\n                        fallbackValue: \"\",\n                        icon: .settings.readIndicatorSetting,\n                        destination: .settings(.postReadIndicator)\n                    )\n                } header: {\n                    Text(\"Differentiate Without Color\")\n                }\n            }\n            \n            Section {\n                Toggle(\"Website Thumbnail Indicator\", icon: .general.browser, isOn: $websiteThumbnailIcon)\n                Toggle(\"Settings Icons\", icon: .settings.settingsIcons, isOn: $showSettingsIcons)\n            } header: {\n                Text(\"Non-Text Indicators\")\n            }\n                       \n            if #available(iOS 18, *) {\n                Section {\n                    NavigationLink(\n                        \"Animated Avatars\",\n                        value: .init(localized: animatedAvatars.label),\n                        fallbackValue: \"\",\n                        icon: .general.playCircle,\n                        destination: .settings(.animatedAvatars)\n                    )\n                } header: {\n                    Text(\"Reduce Motion\")\n                }\n            }\n\n            Section {\n                Toggle(\"Distinguish Interaction Bar\", icon: .general.circle, isOn: $showInteractionBarButtonBackground)\n            } header: {\n                Text(\"Contrast\")\n            }\n            \n            Section {\n                NavigationLink(\n                    \"Slide to Zoom Images\",\n                    value: .init(localized: zoomSliderLocation.label),\n                    fallbackValue: \"\",\n                    icon: .settings.zoomSlider,\n                    destination: .settings(.zoomSlider)\n                )\n            } header: {\n                Text(\"Gestures\")\n            }\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Accessibility\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AccountAdvancedSettingsView.swift",
    "content": "//\n//  AccountAdvancedSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 13/10/2024.\n//\n\nimport SwiftUI\n\nstruct AccountAdvancedSettingsView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    @State var isBot: Bool = false\n    \n    init() {\n        guard let person = AppState.main.firstPerson else { return }\n        _isBot = .init(wrappedValue: person.isBot)\n    }\n    \n    var body: some View {\n        Form {\n            if let updateSettings = appState.firstPerson?.updateSettings {\n                Section {\n                    Toggle(\"Bot Account\", icon: .lemmy.botFlair, isOn: $isBot)\n                        .tint(.themedColorfulAccent(5))\n                        .onChange(of: isBot) {\n                            Task {\n                                do {\n                                    try await updateSettings(.init(isBot: isBot))\n                                } catch {\n                                    handleError(error)\n                                    isBot = appState.firstPerson?.isBot ?? false\n                                }\n                            }\n                        }\n                } footer: {\n                    Text(\"Bot accounts are unable to vote.\")\n                }\n            }\n            if let userAccount = appState.firstAccount as? UserAccount {\n                Section {\n                    Button(\"Refresh Token\") {\n                        navigation.openSheet(.logIn(.reauth(userAccount)))\n                    }\n                }\n            }\n        }\n        .withConditionalLabelStyle()\n        .navigationTitle(\"Advanced\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AccountAgeVisibilitySettingsView.swift",
    "content": "//\n//  AccountAgeVisibilitySettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-04-23.\n//\n\nimport SwiftUI\n\nstruct AccountAgeVisibilitySettingsView: View {\n    @Environment(\\.colorScheme) var colorScheme\n    @Environment(\\.palette) var palette\n    \n    @Setting(\\.person_ageVisibility) var accountAgeVisibility\n    \n    var body: some View {\n        Form {\n            previewSection\n            Section {\n                Text(\"Choose whether to show a user's account age next to their username.\")\n                    .multilineTextAlignment(.center)\n            }\n            Picker(\"Show Account Age\", selection: $accountAgeVisibility) {\n                ForEach(AccountAgeFlairVisibility.allCases, id: \\.self) { visibility in\n                    Text(visibility.label)\n                        .tag(visibility)\n                }\n            }\n            .labelsHidden()\n            .pickerStyle(.inline)\n        }\n        .contentMargins(.top, 16)\n        .navigationTitle(\"Show Account Age\")\n    }\n    \n    @ViewBuilder\n    var previewSection: some View {\n        Section {\n            UnevenRoundedRectangle(\n                cornerRadii: .init(topLeading: 16, bottomLeading: 0, bottomTrailing: 10, topTrailing: 0)\n            )\n            .fill(.themedTertiaryGroupedBackground)\n            .strokeBorder(colorScheme == .light ? .themedSecondaryGroupedBackground : .clear, lineWidth: 2)\n            .frame(height: 100)\n            .overlay(alignment: .topLeading) {\n                HStack(spacing: 0) {\n                    CircleCroppedImageView(url: nil, frame: 30, fallback: .personAvatar)\n                        .opacity(0.8)\n                    HStack(spacing: 5) {\n                        Image(icon: .lemmy.newAccountFlair)\n                            .symbolVariant(.fill)\n                            .imageScale(.small)\n                        Text(flairString)\n                            .fontWeight(.semibold)\n                    }\n                    .padding(.horizontal, 10)\n                    .foregroundStyle(.themedAccountAgeColor(0))\n                    labelText\n                        .lineLimit(1)\n                        .fixedSize(horizontal: false, vertical: true)\n                        .foregroundStyle(.themedSecondary)\n                        .opacity(0.8)\n                        .mask {\n                            LinearGradient(colors: [.black, .black.opacity(0.5)], startPoint: .leading, endPoint: .trailing)\n                        }\n                        .offset(y: -1)\n                }\n                .padding([.top, .leading], 20)\n                .font(.title2)\n            }\n            .padding([.top, .leading], 20)\n            .listRowInsets(.init())\n        }\n    }\n    \n    var flairString: String {\n        let components = DateComponents(day: 5)\n        let formatter = DateComponentsFormatter()\n        formatter.unitsStyle = .abbreviated\n        return formatter.string(from: components) ?? \"\"\n    }\n    \n    var labelText: Text {\n        let string = String(\n            localized: \"john@example.com\",\n            // swiftlint:disable:next line_length\n            comment: \"Translate \\\"john\\\" into the equivalent placeholder name in your language, and \\\"example.com\\\" into a suitable example domain for your locale. The placeholder name should be as short as possible, as this string is displayed in contexts where there may not be much space horizontally.\"\n        )\n        let parts = string.split(separator: \"@\")\n        guard parts.count == 2 else {\n            assertionFailure()\n            return Text(string)\n        }\n        return Text(parts[0]) + Text(verbatim: \"@\\(parts[1])\").foregroundColor(palette.label.tertiary)\n    }\n}\n\nenum AccountAgeFlairVisibility: String, Codable, CaseIterable {\n    case always, newAccountsOnly, never\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .always:\n            \"Always\"\n        case .newAccountsOnly:\n            \"For New Accounts Only\"\n        case .never:\n            \"Never\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AccountContentSettingsView.swift",
    "content": "//\n//  AccountGeneralSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 13/10/2024.\n//\n\nimport SwiftUI\nimport MlemMiddleware\n\nstruct AccountContentSettingsView: View {\n    @Environment(AppState.self) var appState\n    \n    @State var showNsfw: Bool = false\n    @State var showBotAccounts: Bool = false\n    @State var sendNotificationsToEmail: Bool = false\n    \n    init() {\n        guard let person = AppState.main.firstPerson else { return }\n        _showNsfw = .init(wrappedValue: person.showNsfw.value_ ?? false)\n        _showBotAccounts = .init(wrappedValue: person.showBotAccounts.value_ ?? false)\n        _sendNotificationsToEmail = .init(wrappedValue: person.sendNotificationsToEmail.value_ ?? false)\n    }\n    \n    var body: some View {\n        if let updateSettings = appState.firstPerson?.updateSettings {\n            content(updateSettings: updateSettings)\n        } else {\n            ProgressView()\n        }\n    }\n    \n    // swiftlint:disable:next function_body_length\n    func content(updateSettings: @escaping (Person.ProfileSettings) async throws -> Void) -> some View {\n        Form {\n            Section {\n                Toggle(\"Show NSFW Content\", icon: .settings.blurNsfw, isOn: $showNsfw)\n                    .tint(.themedWarning)\n                    .onChange(of: showNsfw) {\n                        Task {\n                            do {\n                                try await updateSettings(.init(showNsfw: showNsfw))\n                            } catch {\n                                handleError(error)\n                                showNsfw = appState.firstPerson?.showNsfw.value_ ?? false\n                            }\n                        }\n                    }\n            } footer: {\n                Text(\"Show content flagged as Not Safe For Work.\")\n            }\n            Section {\n                Toggle(\"Show Bot Accounts\", icon: .lemmy.botFlair, isOn: $showBotAccounts)\n                    .onChange(of: showBotAccounts) {\n                        Task {\n                            do {\n                                try await updateSettings(.init(showBotAccounts: showBotAccounts))\n                            } catch {\n                                handleError(error)\n                                showBotAccounts = appState.firstPerson?.showBotAccounts.value_ ?? false\n                            }\n                        }\n                    }\n            }\n            Section {\n                Toggle(\"Send Notifications to Email\", icon: .general.email, isOn: $sendNotificationsToEmail)\n                    .onChange(of: sendNotificationsToEmail) {\n                        Task {\n                            do {\n                                try await updateSettings(.init(sendNotificationsToEmail: sendNotificationsToEmail))\n                            } catch {\n                                handleError(error)\n                                sendNotificationsToEmail = appState.firstPerson?.sendNotificationsToEmail.value_ ?? false\n                            }\n                        }\n                    }\n                    .disabled(appState.firstPerson?.email == nil)\n            } footer: {\n                if let email = appState.firstPerson?.email.value as? String {\n                    Text(\"Notifications will be sent to \\(email).\")\n                } else {\n                    Text(\"You don't have an email attached to this account.\")\n                }\n            }\n            \n            Section {\n                NavigationLink(\n                    \"Discussion Languages\",\n                    icon: .settings.language,\n                    destination: .settings(.accountLanguages)\n                )\n            }\n        }\n        .withConditionalLabelStyle()\n        .navigationTitle(\"Content & Notifications\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AccountEmailSettingsView.swift",
    "content": "//\n//  AccountEmailSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 13/10/2024.\n//\n\nimport SwiftUI\n\nimport MlemMiddleware\n\nstruct AccountEmailSettingsView: View {\n    @Environment(AppState.self) var appState\n    @Environment(\\.dismiss) var dismiss\n    \n    @State var email: String = \"\"\n    @State var isSubmitting: Bool = false\n    @FocusState var isFocused\n    \n    init() {\n        guard let person = AppState.main.firstPerson else { return }\n        _email = .init(wrappedValue: person.email.value as? String ?? \"\")\n    }\n    \n    var showToolbarOptions: Bool {\n        email != appState.firstPerson?.email.value\n    }\n    \n    var body: some View {\n        Form {\n            TextField(\"Email\", text: $email)\n                .focused($isFocused)\n                .onAppear {\n                    isFocused = true\n                }\n        }\n        .navigationBarBackButtonHidden(showToolbarOptions)\n        .disabled(isSubmitting)\n        .toolbar {\n            if showToolbarOptions {\n                ToolbarItem(placement: .topBarLeading) {\n                    Button(\"Cancel\") {\n                        email = appState.firstPerson?.email as? String ?? \"\"\n                    }\n                    .disabled(isSubmitting)\n                }\n                ToolbarItem(placement: .topBarTrailing) {\n                    if isSubmitting {\n                        ProgressView()\n                    } else if let updateSettings = appState.firstPerson?.updateSettings {\n                        Button(\"Save\") {\n                            Task { @MainActor in\n                                await submit(updateSettings: updateSettings)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n    \n    @MainActor\n    func submit(updateSettings: (Person.ProfileSettings) async throws -> Void) async {\n        isSubmitting = true\n        do {\n            try await updateSettings(.init(email: email))\n        } catch {\n            handleError(error)\n            email = appState.firstPerson?.email as? String ?? \"\"\n        }\n        isSubmitting = false\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AccountListSettingsView.swift",
    "content": "//\n//  AccountListSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/05/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct AccountListSettingsView: View {\n    @Environment(AppState.self) var appState\n    @Setting(\\.accounts_keepPlace) var keepPlace\n    @Setting(\\.accounts_preferredListRowComplication) var preferredListRowComplication\n\n    var accounts: [UserAccount] { AccountsTracker.main.userAccounts }\n    \n    var body: some View {\n        Form {\n            headerView\n            AccountListView()\n            Section {\n                Toggle(\"Reload on Switch\", icon: .lemmy.switchAccountAndReload, isOn: $keepPlace.invert())\n                Toggle(\n                    \"Show Response Times\",\n                    icon: .general.time,\n                    isOn: .init(\n                        get: { preferredListRowComplication == .responseTime },\n                        set: { preferredListRowComplication = $0 ? .responseTime : .lastUsed }\n                    )\n                )\n            }\n        }\n        .withConditionalLabelStyle()\n        .hiddenNavigationTitle(\"Accounts\")\n    }\n    \n    @ViewBuilder\n    var headerView: some View {\n        // empty section disables background\n        Section {} header: {\n            VStack(alignment: .center) {\n                Group {\n                    if accounts.count >= 2 {\n                        AvatarStackView(\n                            urls: accounts.map(\\.avatar),\n                            fallback: .personAvatar,\n                            height: 64,\n                            spacing: 42,\n                            outlineWidth: 1\n                        )\n                    } else {\n                        Image(systemName: \"person.3.fill\")\n                            .resizable()\n                            .aspectRatio(contentMode: .fit)\n                            .symbolRenderingMode(.hierarchical)\n                            .foregroundStyle(.blue)\n                    }\n                }\n                .frame(height: 64)\n                \n                Text(\"Accounts\")\n                    .font(.title)\n                    .fontWeight(.bold)\n            }\n            .frame(maxWidth: .infinity)\n            .foregroundStyle(.themedPrimary) // override default .secondary style\n        }\n        .textCase(nil) // override default all-caps\n        .listRowInsets(.init(top: 40, leading: 0, bottom: 0, trailing: 0))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AccountLocalSettingsView.swift",
    "content": "//\n//  AccountLocalSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-02.\n//\n\nimport SwiftUI\n\nstruct AccountLocalSettingsView: View {\n    @Environment(AppState.self) var appState\n    \n    @State var isShowingFavoriteDeletionWarning: Bool = false\n    @State var isShowingClearVisitHistoryWarning: Bool = false\n    @State var isShowingDisableVisitHistoryWarning: Bool = false\n\n    var body: some View {\n        Form {\n            AccountNicknameFieldView()\n            if let userSession = AppState.main.firstSession as? UserSession {\n                communityFavoritesSection(userSession)\n                Section {\n                    visitHistoryToggle(userSession)\n                    if let visitHistory = userSession.visitHistory {\n                        clearVisitHistoryButton(userSession, visitHistory: visitHistory)\n                    }\n                }\n            }\n        }\n        .navigationTitle(\"Local Options\")\n    }\n    \n    @ViewBuilder\n    func communityFavoritesSection(_ session: UserSession) -> some View {\n        Section {\n            Button(\"Delete Community Favorites\", icon: .general.delete, role: .destructive) {\n                isShowingFavoriteDeletionWarning = true\n            }\n            .disabled(session.account.favorites.isEmpty)\n            .tint(.themedWarning)\n            .confirmationDialog(\n                \"Delete Community Favorites\",\n                isPresented: $isShowingFavoriteDeletionWarning\n            ) {\n                Button(\"Delete\", role: .destructive) {\n                    for community in session.subscriptions.favorites {\n                        guard let updateFavorite = community.updateFavorite else {\n                            assertionFailure(\"updateFavorite not present yet\")\n                            return\n                        }\n                        updateFavorite(false)\n                    }\n                }\n            } message: {\n                Text(\"Are you sure you want to delete all community favorites for this account? This cannot be undone.\")\n            }\n        } footer: {\n            if session.account.favorites.isEmpty {\n                Text(\"This account has no favorite communities.\")\n            } else {\n                Text(\"This account has \\(session.account.favorites.count) favorite communities.\")\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func visitHistoryToggle(_ session: UserSession) -> some View {\n        Toggle(\n            \"Remember Search History\",\n            isOn: .init(\n                get: { session.account.visitHistoryEnabled },\n                set: { newValue in\n                    if newValue || (session.visitHistory?.isEmpty ?? true) {\n                        Task { @MainActor in\n                            try await session.setVisitHistoryEnabled(newValue)\n                        }\n                    } else {\n                        isShowingDisableVisitHistoryWarning = true\n                    }\n                }\n            )\n        )\n        .confirmationDialog(\n            \"Turn off search history?\",\n            isPresented: $isShowingDisableVisitHistoryWarning,\n            titleVisibility: .visible\n        ) {\n            Button(\"Turn Off\", role: .destructive) {\n                Task { @MainActor in\n                    do {\n                        try await session.setVisitHistoryEnabled(false)\n                    } catch {\n                        handleError(error)\n                    }\n                }\n            }\n            Button(\"Cancel\", role: .cancel) {}\n        } message: {\n            Text(\"This will clear your recent searches, which cannot be undone.\")\n        }\n    }\n    \n    @ViewBuilder\n    func clearVisitHistoryButton(_ session: UserSession, visitHistory: VisitHistory) -> some View {\n        if let visitHistory = session.visitHistory {\n            Button(\"Clear Search History\", icon: .general.delete, role: .destructive) {\n                isShowingClearVisitHistoryWarning = true\n            }\n            .tint(.themedWarning)\n            .confirmationDialog(\n                \"Clear search history?\",\n                isPresented: $isShowingClearVisitHistoryWarning,\n                titleVisibility: .visible\n            ) {\n                Button(\"Clear\", role: .destructive) {\n                    visitHistory.clear()\n                    Task {\n                        do {\n                            try await session.saveVisitHistory()\n                        } catch {\n                            handleError(error)\n                        }\n                    }\n                }\n                Button(\"Cancel\", role: .cancel) {}\n            } message: {\n                Text(\"This action cannot be undone.\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AccountNicknameFieldView.swift",
    "content": "//\n//  AccountNicknameFieldView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-02.\n//\n\nimport SwiftUI\n\nstruct AccountNicknameFieldView: View {\n    @Environment(AppState.self) var appState\n    \n    @Setting(\\.tab_profile_labelType) var tabProfileLabelType\n\n    @State var nickname: String\n\n    init() {\n        self.nickname = AppState.main.firstAccount.storedNickname ?? \"\"\n    }\n    \n    var body: some View {\n        Section(\"Nickname\") {\n            TextField(\n                \"Nickname\",\n                text: $nickname,\n                prompt: Text(appState.firstAccount.name)\n            )\n            .onSubmit {\n                AppState.main.firstAccount.setNickname(nickname)\n            }\n        } footer: {\n            if tabProfileLabelType == .nickname {\n                Text(\"The name shown in the account switcher and tab bar.\")\n            } else {\n                Text(\"The name shown in the account switcher.\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AccountSettingsView.swift",
    "content": "//\n//  AccountSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 09/05/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct AccountSettingsView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.dismiss) var dismiss\n    \n    @State private var showingSignOutConfirmation: Bool = false\n    \n    var body: some View {\n        Form {\n            // empty section disables background\n            Section {} header: {\n                Group {\n                    if let userAccount = appState.firstSession as? UserSession {\n                        ProfileHeaderView(userAccount.person)\n                    } else {\n                        ProfileHeaderView(appState.firstSession.instance)\n                    }\n                }\n                .foregroundStyle(.themedPrimary) // override default .secondary style\n            }\n            .textCase(nil) // override default all-caps\n            .listRowInsets(.init(top: 10, leading: 0, bottom: 0, trailing: 0))\n            \n            if appState.firstSession is UserSession {\n                Section {\n                    if appState.firstAccount.siteSoftware?.supports(.editProfile) ?? false {\n                        NavigationLink(\n                            \"My Profile\",\n                            icon: .lemmy.person,\n                            destination: .settings(.profile)\n                        )\n                        .gradientTint(.themedColorfulAccent(5))\n                    }\n                    if appState.firstAccount.siteSoftware?.supports(.editAccountSettings) ?? false {\n                        NavigationLink(\n                            \"Sign-In & Security\",\n                            icon: .general.security,\n                            destination: .settings(.accountSignIn)\n                        )\n                        .gradientTint(.themedColorfulAccent(2))\n                        NavigationLink(\n                            \"Content & Notifications\",\n                            icon: .lemmy.post,\n                            destination: .settings(.accountContent)\n                        )\n                        .gradientTint(.themedColorfulAccent(0))\n                        NavigationLink(\n                            \"Advanced\",\n                            icon: .settings.advanced,\n                            destination: .settings(.accountAdvanced)\n                        )\n                        .gradientTint(.themedNeutralAccent)\n                    }\n                }\n                Section {\n                    NavigationLink(\n                        \"Block List\",\n                        icon: .lemmy.block,\n                        destination: .blockList\n                    )\n                    .gradientTint(.themedNegative)\n                }\n                Section {\n                    NavigationLink(\n                        \"Local Options\",\n                        icon: .settings.localAccountOptions,\n                        destination: .settings(.accountLocal)\n                    )\n                    .gradientTint(.themedColorfulAccent(2))\n                } footer: {\n                    Text(\"These options are stored locally in Mlem and not on your Lemmy account.\")\n                }\n            } else {\n                AccountNicknameFieldView()\n            }\n            \n            Group {\n                Section {\n                    Button {\n                        appState.firstAccount.signOut()\n                    } label: {\n                        Text(signOutLabel)\n                            .frame(maxWidth: .infinity)\n                    }\n                    .confirmationDialog(String(localized: signOutPrompt), isPresented: $showingSignOutConfirmation) {\n                        Button(String(localized: signOutLabel), role: .destructive) {\n                            appState.firstAccount.signOut()\n                        }\n                    } message: {\n                        Text(signOutPrompt)\n                    }\n                }\n                \n                if let account = appState.firstAccount as? UserAccount {\n                    Section {\n                        Button(role: .destructive) {\n                            navigation.openSheet(.deleteAccount(account))\n                        } label: {\n                            Text(\"Delete Account\")\n                                .frame(maxWidth: .infinity)\n                        }\n                    }\n                }\n            }\n            .tint(.themedWarning)\n        }\n        .labelStyle(.squircle)\n        .navigationTitle(Text(\"Account\"))\n    }\n    \n    var title: String {\n        if let userAccount = appState.firstSession as? UserSession {\n            return userAccount.person?.displayName ?? \"Account\"\n        } else {\n            return appState.firstSession.instance?.displayName ?? \"User\"\n        }\n    }\n    \n    var subtitle: String {\n        if let userAccount = appState.firstSession as? UserSession {\n            return userAccount.person?.fullNameWithPrefix ?? \"Loading...\"\n        }\n        return appState.firstSession.instance?.name ?? \"Loading...\"\n    }\n    \n    var signOutLabel: LocalizedStringResource {\n        appState.firstAccount is UserAccount ? \"Sign Out\" : \"Remove\"\n    }\n    \n    var signOutPrompt: LocalizedStringResource {\n        if appState.firstAccount is UserAccount {\n            \"Really sign out of \\(appState.firstAccount.nickname)?\"\n        } else {\n            \"Really remove \\(appState.firstAccount.nickname)?\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AccountSignInSettingsView.swift",
    "content": "//\n//  AccountSignInSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 13/10/2024.\n//\n\nimport SwiftUI\n\nstruct AccountSignInSettingsView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    var body: some View {\n        Form {\n            Section {\n                NavigationLink(.settings(.accountChangeEmail)) {\n                    HStack {\n                        Text(\"Email\")\n                        Spacer()\n                        Text(appState.firstPerson?.email.value as? String ?? \"\")\n                            .foregroundStyle(.themedSecondary)\n                    }\n                }\n            }\n            Section {\n                Button(\"Change Password\", icon: .general.security) {\n                    navigation.openSheet(.settings(.accountChangePassword))\n                }\n            }\n        }\n        .withConditionalLabelStyle()\n        .navigationTitle(\"Sign-In & Security\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AdvancedSettingsView.swift",
    "content": "//\n//  AdvancedSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2024.\n//\n\nimport Nuke\nimport SwiftUI\n\nstruct AdvancedSettingsView: View {\n    var body: some View {\n        Form {\n            Section {\n                HStack {\n                    Text(\"Cache\")\n                    Spacer()\n                    TimelineView(.periodic(from: .now, by: 0.5)) { _ in\n                        Text(ByteCountFormatter.string(fromByteCount: Int64(URLCache.shared.currentDiskUsage), countStyle: .file))\n                            .foregroundStyle(.themedSecondary)\n                    }\n                }\n            }\n            header: {\n                Text(\"Disk Usage\")\n            }\n            footer: {\n                // Nesting \"500 MB\" so we can change it later without re-localizing\n                Text(\"Images are cached on your device for fast reuse. The maximum cache size is around \\(\"500 MB\").\")\n            }\n            Button(\"Clear Cache\") {\n                URLCache.shared.removeAllCachedResponses()\n                ImagePipeline.shared.cache.removeAll()\n                ToastModel.main.add(.success(\"Cache Cleared\"))\n            }\n            Section {\n                NavigationLink(\"Developer\", destination: .settings(.developer))\n            }\n        }\n        .navigationTitle(\"Advanced\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/AnimatedAvatarSettingsView.swift",
    "content": "//\n//  AnimatedAvatarSettingsView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-15.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct AnimatedAvatarSettingsView: View {\n    @Setting(\\.media_animatedAvatars) var animatedAvatars\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Animated Avatars\",\n                description: \"Some users set animated media as their avatar. Control whether these avatars should play their animations.\",\n                icon: .general.playCircle\n            )\n            .gradientTint(.themedColorfulAccent(4))\n            \n            Picker(\"Animate Avatars...\", selection: $animatedAvatars) {\n                ForEach(AnimatedAvatarBehavior.allCases, id: \\.self) { location in\n                    Label(location.label.key, icon: location.icon)\n                        .symbolVariant(.circle)\n                        .tag(location)\n                }\n            }\n            .labelsHidden()\n            .pickerStyle(.inline)\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Animated Avatars\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/BlockListView.swift",
    "content": "//\n//  BlockListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-11-09.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct BlockListView: View {\n    @Environment(AppState.self) var appState\n    \n    enum Tab: CaseIterable, Identifiable {\n        case people, communities, instances\n        \n        var id: Self { self }\n        \n        var label: LocalizedStringResource {\n            switch self {\n            case .people: \"Users\"\n            case .communities: \"Communities\"\n            case .instances: \"Instances\"\n            }\n        }\n    }\n    \n    @State var selectedTab: Tab = .people\n    @State var people: [Person] = []\n    @State var communities: [Community] = []\n    @State var instances: [Instance] = []\n    \n    var body: some View {\n        FancyScrollView {\n            BubblePicker(availableTabs, selected: $selectedTab, label: \\.label, value: { tab in\n                guard let blockList = (appState.firstSession as? UserSession)?.blocks else { return 0 }\n                switch tab {\n                case .people:\n                    return blockList.personCount\n                case .communities:\n                    return blockList.communityCount\n                case .instances:\n                    return blockList.instanceCount\n                }\n            })\n            switch selectedTab {\n            case .people:\n                SearchResultsView(results: people.filter(\\.blocked_.realizedValue)) { person in\n                    PersonListRow(person, showBlockStatus: false)\n                }\n            case .communities:\n                SearchResultsView(results: communities.filter(\\.blocked_.realizedValue)) { community in\n                    CommunityListRow(community, showBlockStatus: false)\n                }\n            case .instances:\n                SearchResultsView(results: instances.filter(\\.blocked_.realizedValue)) { instance in\n                    InstanceListRow(instance, showBlockStatus: false)\n                }\n            }\n        }\n        .themedGroupedBackground()\n        .onAppear {\n            Task { @MainActor in\n                do {\n                    let result = try await appState.firstApi.getBlocked()\n                    people = result.people\n                    communities = result.communities\n                    instances = result.instances\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n        .navigationTitle(\"Block List\")\n    }\n    \n    var availableTabs: [Tab] {\n        var output: [Tab] = [.people, .communities]\n        if appState.firstApi.supports(.viewInstanceBlockList, defaultValue: false) {\n            output.append(.instances)\n        }\n        return output\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ChangePasswordView.swift",
    "content": "//\n//  ChangePasswordView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-25.\n//\n\nimport Haptics\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ChangePasswordView: View {\n    enum ViewState {\n        case initial, waiting, success\n    }\n    \n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.dismiss) var dismiss\n\n    @State private var viewState: ViewState = .initial\n    \n    @State var newPassword: String = \"\"\n    @State var confirmNewPassword: String = \"\"\n    @State var currentPassword: String = \"\"\n    \n    enum FocusedField {\n        case newPassword, confirmNewPassword, currentPassword\n    }\n\n    @FocusState private var focusedField: FocusedField?\n    \n    var canSave: Bool {\n        !currentPassword.isEmpty\n            && !newPassword.isEmpty\n            && newPassword == confirmNewPassword\n            && (10 ... 60 ~= newPassword.count)\n    }\n    \n    var body: some View {\n        Form {\n            Group {\n                Section {\n                    SecureField(\"New Password\", text: $newPassword)\n                        .focused($focusedField, equals: .newPassword)\n                    SecureField(\"Confirm New Password\", text: $confirmNewPassword)\n                        .focused($focusedField, equals: .confirmNewPassword)\n                }\n                \n                Section {\n                    SecureField(\"Current Password\", text: $currentPassword)\n                        .focused($focusedField, equals: .currentPassword)\n                }\n            }\n            .disabled(viewState != .initial)\n            Section {\n                Button(action: submit) {\n                    switch viewState {\n                    case .initial:\n                        Text(\"Save\")\n                            .transition(.scale(scale: 0.9).combined(with: .opacity))\n                    case .waiting:\n                        ProgressView()\n                            .transition(.scale(scale: 0.9).combined(with: .opacity))\n                    case .success:\n                        Image(icon: .general.success)\n                            .symbolVariant(.circle.fill)\n                            .foregroundStyle(.themedPositive)\n                            .transition(.scale(scale: 0.9).combined(with: .opacity))\n                    }\n                }\n                .animation(.easeOut(duration: 0.1), value: viewState)\n                .frame(maxWidth: .infinity)\n                .disabled(!canSave)\n            }\n            Section {\n                Button(\"Cancel\") {\n                    dismiss()\n                }\n                .frame(maxWidth: .infinity)\n                .disabled(viewState != .initial)\n            } footer: {\n                Group {\n                    if !newPassword.isEmpty {\n                        if newPassword != confirmNewPassword {\n                            Text(\"Passwords don't match.\")\n                        } else if !(10 ... 60 ~= newPassword.count) {\n                            Text(\"New password must be between \\(10) and \\(60) characters long.\")\n                        }\n                    }\n                }\n                .foregroundStyle(.themedWarning)\n            }\n        }\n        .onAppear {\n            focusedField = .newPassword\n        }\n    }\n    \n    func submit() {\n        if viewState == .initial {\n            Task { @MainActor in\n                do {\n                    viewState = .waiting\n                    try await appState.firstApi.changePassword(\n                        newPassword: newPassword,\n                        confirmNewPassword: confirmNewPassword,\n                        oldPassword: currentPassword\n                    )\n                    AccountsTracker.main.saveAccounts(ofType: .user)\n                    viewState = .success\n                    hapticManager.play(haptic: .success, tier: .high)\n                    try? await Task.sleep(for: .seconds(0.5))\n                    dismiss()\n                    // Catch separately to prevent the token expiry sheet opening in this view\n                } catch ApiClientError.invalidSession {\n                    ToastModel.main.add(.failure(\"Current password is incorrect\"))\n                    viewState = .initial\n                } catch let ApiClientError.response(response, _) where response.error == \"invalid_password\" {\n                    ToastModel.main.add(.failure(\"New password is invalid\"))\n                    viewState = .initial\n                } catch {\n                    handleError(error)\n                    viewState = .initial\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/CommentJumpButtonSettingsView.swift",
    "content": "//\n//  CommentJumpButtonSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-22.\n//\n\nimport SwiftUI\n\nstruct CommentJumpButtonSettingsView: View {\n    @Environment(\\.colorScheme) var colorScheme\n    \n    @Setting(\\.comment_jumpButton) var jumpButton\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Jump Button\",\n                description: \"Tap on the Jump Button whilst viewing a comment thread to scroll to the next comment.\"\n            ) {\n                Image(icon: .lemmy.jumpButton)\n                    .font(.title)\n                    .fontWeight(.semibold)\n                    .foregroundStyle(.themedSecondary)\n                    .aspectRatio(contentMode: .fill)\n                    .padding(25)\n                    .background(\n                        Circle()\n                            .stroke(.themedTertiary.opacity(0.3), lineWidth: 3)\n                            .background(.ultraThinMaterial)\n                            .clipShape(.circle)\n                    )\n                    .compositingGroup()\n                    .shadow(color: colorScheme == .dark ? .black.opacity(0.5) : .clear, radius: 10, y: 5)\n            }\n            Section {\n                Toggle(\n                    \"Jump Button\",\n                    icon: .lemmy.jumpButton,\n                    isOn: .init(get: { jumpButton != .none }, set: { jumpButton = $0 ? .bottomTrailing : .none })\n                )\n                .symbolVariant(.circle)\n            }\n            if jumpButton != .none {\n                Section(\"Alignment\") {\n                    Picker(\"Jump Button\", selection: $jumpButton) {\n                        ForEach(pickerCases, id: \\.self) { location in\n                            Label(location.label.key, icon: location.icon)\n                                .symbolVariant(.circle)\n                        }\n                    }\n                    .labelsHidden()\n                    .pickerStyle(.inline)\n                }\n            }\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .animation(.easeOut(duration: 0.1), value: jumpButton == .none)\n        .hiddenNavigationTitle(\"Jump Button\")\n    }\n    \n    var pickerCases: [CommentJumpButtonLocation] {\n        [.bottomLeading, .bottomCenter, .bottomTrailing]\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/CommentMaximumDepthSettingsView.swift",
    "content": "//\n//  CommentMaximumDepthSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-21.\n//\n\nimport SwiftUI\n\nstruct CommentMaximumDepthSettingsView: View {\n    @Setting(\\.comment_maxDepth) var maxCommentDepth\n    \n    var body: some View {\n        Form {\n            Section {\n                HStack {\n                    Label(\"Maximum Comment Depth\", icon: .settings.commentDepth)\n                    Spacer()\n                    Text(String(maxCommentDepth))\n                        .foregroundStyle(.themedSecondary)\n                        .monospaced()\n                }\n                Slider(\n                    value: .init(\n                        get: { Double(maxCommentDepth) },\n                        set: { maxCommentDepth = Int($0) }\n                    ),\n                    in: 1.0 ... 12.0,\n                    step: 1\n                )\n            } footer: {\n                Text(\"The number of child comments that can appear in a chain before the \\\"More Replies\\\" button is shown.\")\n            }\n        }\n        .navigationTitle(\"Maximum Depth\")\n        .withConditionalLabelStyle()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/CommentSettingsView.swift",
    "content": "//\n//  CommentSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2024.\n//\n\nimport SwiftUI\n\nstruct CommentSettingsView: View {\n    @Setting(\\.comment_compact) var compactComments\n    @Setting(\\.comment_gestures_tapToCollapse) var tapCommentsToCollapse\n    @Setting(\\.comment_maxDepth) var maxCommentDepth\n    @Setting(\\.comment_jumpButton) var jumpButton\n    @Setting(\\.comment_showDownvotesCompact) var showDownvotesCompact\n    @Setting(\\.interactionBar_comment) var commentInteractionBar\n\n    var body: some View {\n        Form {\n            Section(\"Size\") {\n                HStack {\n                    sizePickerItem(\"Large\", isOn: false)\n                    sizePickerItem(\"Compact\", isOn: true)\n                }\n            }\n            Section {\n                NavigationLink(.settings(.interactionBar(.comment))) {\n                    SettingsInteractionBarSummaryView(configuration: commentInteractionBar)\n                }\n                NavigationLink(\"Swipe Actions\", destination: .settings(.swipeActions(.comment)))\n            }\n            Section {\n                NavigationLink(\n                    \"Jump Button\",\n                    value: .init(localized: jumpButton.label),\n                    fallbackValue: \"\",\n                    icon: .settings.jumpButton,\n                    destination: .settings(.commentJumpButton)\n                )\n                NavigationLink(\n                    \"Maximum Depth\",\n                    value: String(maxCommentDepth),\n                    fallbackValue: \"\",\n                    icon: .settings.commentDepth,\n                    destination: .settings(.commentMaximumDepth)\n                )\n                \n        if compactComments {\n                Toggle(\"Show Downvotes Separately\", icon: .lemmy.votes, isOn: $showDownvotesCompact)\n        }\n            }\n            Section {\n                Toggle(\"Tap to Collapse\", icon: .general.collapse, isOn: $tapCommentsToCollapse)\n            }\n        }\n        .withConditionalLabelStyle()\n        .navigationTitle(\"Comments\")\n    }\n    \n    var sizePickerCommentPreviewDepths: [CGFloat] {\n        [0, 1, 2, 1, 2, 3, 2, 1, 2, 2]\n    }\n    \n    @ViewBuilder\n    func sizePickerItem(_ titleKey: LocalizedStringResource, isOn: Bool) -> some View {\n        DevicePickerItem(titleKey, item: isOn, selected: $compactComments, scale: 1.2) {\n            VStack(spacing: 3) {\n                ForEach(Array(sizePickerCommentPreviewDepths.enumerated()), id: \\.offset) { _, depth in\n                    RoundedRectangle(cornerRadius: 2)\n                        .frame(height: isOn ? 10 : 15)\n                        .padding(.leading, depth * 4)\n                }\n            }\n            .padding(.top, 4)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/CommunitySettingsView.swift",
    "content": "//\n//  CommunitySettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-05.\n//\n\nimport SwiftUI\n\nstruct CommunitySettingsView: View {\n    @Setting(\\.community_showAvatar) var showCommunityAvatar\n\n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Communities\",\n                description: \"Customize the appearance of communities.\",\n                icon: .lemmy.community\n            )\n            .gradientTint(.themedCommunityAccent)\n            Section {\n                NavigationLink(\"Subscription List\", destination: .settings(.subscriptionList))\n                NavigationLink(\"Swipe Actions\", destination: .settings(.swipeActions(.community)))\n            }\n            Section {\n                Toggle(\"Community Avatar\", icon: .lemmy.community, isOn: $showCommunityAvatar)\n                    .symbolVariant(.circle)\n            } footer: {\n                Text(\"Choose whether to show community avatars on posts.\")\n            }\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Communities\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Components/ConditionalLabelStyleViewModifier.swift",
    "content": "//\n//  ConditionalLabelStyleViewModifier.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-08-25.\n//\n\nimport SwiftUI\nimport Icons\n\nprivate struct ConditionalLabelStyleViewModifier: ViewModifier {\n    func body(content: Content) -> some View {\n        content\n            .labelStyle(ConditionalIconLabelStyle())\n            .toggleStyle(ConditionalIconToggleStyle())\n    }\n}\n\nextension View {\n    func withConditionalLabelStyle() -> some View {\n        modifier(ConditionalLabelStyleViewModifier())\n    }\n}\n\nprivate struct ConditionalIconToggleStyle: ToggleStyle {\n    @Setting(\\.a11y_showSettingsIcons) var showSettingsIcons\n    \n    func makeBody(configuration: Configuration) -> some View {\n        Toggle(isOn: Binding(get: { configuration.isOn },\n                             set: { configuration.isOn = $0 })) {\n            configuration.label\n                .labelStyle(ConditionalIconLabelStyle())\n        }\n    }\n}\n\nprivate struct ConditionalIconLabelStyle: LabelStyle {\n    @Setting(\\.a11y_showSettingsIcons) var showSettingsIcons\n    \n    func makeBody(configuration: Configuration) -> some View {\n        Label {\n            configuration.title\n        } icon: {\n            if showSettingsIcons {\n                configuration.icon.foregroundStyle(.themedAccent)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Components/DevicePickerItem.swift",
    "content": "//\n//  DevicePickerItem.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-21.\n//\n\nimport Haptics\nimport SwiftUI\n\nstruct DevicePickerItem<Item: Equatable, ScreenContent: View>: View {\n    @Environment(HapticManager.self) var hapticManager\n    \n    let title: String\n    let item: Item\n    let scale: CGFloat\n    @Binding var selected: Item\n    @ViewBuilder var screenContent: () -> ScreenContent\n    \n    init(\n        _ titleKey: LocalizedStringResource,\n        item: Item,\n        selected: Binding<Item>,\n        scale: CGFloat = 1.0,\n        @ViewBuilder screenContent: @escaping () -> ScreenContent\n    ) {\n        self.title = .init(localized: titleKey)\n        self.item = item\n        self.scale = scale\n        self._selected = selected\n        self.screenContent = screenContent\n    }\n    \n    var isSelected: Bool { item == selected }\n    \n    var body: some View {\n        VStack {\n            SettingsDeviceView(selected: isSelected, scale: scale, screenContent: screenContent)\n            Text(title)\n                .lineLimit(1)\n                .foregroundStyle(isSelected ? .themedContrastingLabel : .themedPrimary)\n                .padding(.vertical, 5)\n                .padding(.horizontal, 10)\n                .background(isSelected ? .themedAccent : .clear, in: .capsule)\n        }\n        .onTapGesture {\n            hapticManager.play(haptic: .gentleInfo, tier: .low)\n            withAnimation(.easeOut(duration: 0.1)) {\n                selected = item\n            }\n        }\n        .frame(maxWidth: .infinity)\n        .font(.footnote)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Components/SettingsDeviceView.swift",
    "content": "//\n//  SettingsDeviceView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-17.\n//\n\nimport SwiftUI\n\nstruct SettingsDeviceView<ScreenContent: View>: View {\n    @Environment(\\.colorScheme) var colorScheme\n    \n    var screenContent: ScreenContent\n    var selected: Bool\n    var screenPadding: Bool\n    var scale: CGFloat\n    \n    var accentColor: AnyShapeStyle {\n        if selected {\n            return .init(.tint)\n        }\n        return .init(.themedNeutralAccent)\n    }\n    \n    init(\n        selected: Bool = false,\n        screenPadding: Bool = true,\n        scale: CGFloat = 1.0,\n        @ViewBuilder screenContent: @escaping () -> ScreenContent\n    ) {\n        self.selected = selected\n        self.screenPadding = screenPadding\n        self.screenContent = screenContent()\n        self.scale = scale\n    }\n    \n    var body: some View {\n        frameView\n            .aspectRatio(aspectRatio, contentMode: .fit)\n            .frame(maxWidth: (UIDevice.isPad ? 100 : 40) * scale)\n            .compositingGroup()\n            .opacity(colorScheme == .dark && !selected ? 0.6 : 1)\n    }\n    \n    var aspectRatio: CGFloat {\n        if UIDevice.isPad {\n            return 3 / 4\n        }\n        if UIDevice.frameType == .noNotch {\n            return 9 / 16\n        }\n        return 9 / 19\n    }\n    \n    var frameCornerRadiusScaleFactor: CGFloat {\n        if UIDevice.isPad {\n            return 1 / 12\n        }\n        if UIDevice.frameType == .noNotch {\n            return 1 / 8\n        }\n        return 1 / 6\n    }\n    \n    var generalPadding: CGFloat {\n        scale * 3\n    }\n    \n    var frameView: some View {\n        GeometryReader { geometry in\n            RoundedRectangle(cornerRadius: geometry.size.width * frameCornerRadiusScaleFactor)\n                .fill(accentColor.opacity(0.1))\n                .strokeBorder(accentColor, lineWidth: 2)\n                .overlay(alignment: .top) {\n                    if UIDevice.frameType != .noNotch {\n                        notchView(geometry: geometry)\n                            .padding(.top, 2)\n                    }\n                }\n                .background {\n                    let radius = geometry.size.width * frameCornerRadiusScaleFactor - 4\n                    Color.clear\n                        .background(alignment: .top) {\n                            VStack(spacing: generalPadding) {\n                                screenContent\n                                    .foregroundStyle(accentColor)\n                                    .frame(maxWidth: .infinity, maxHeight: .infinity)\n                                    .fixedSize(horizontal: false, vertical: true)\n                            }\n                        }\n                        .clipShape(.rect(cornerRadius: radius))\n                        .mask {\n                            LinearGradient(\n                                colors: .init(repeating: .black, count: 8) + [\n                                    .black.opacity(0.9), .black.opacity(0.7), .black.opacity(0.5)\n                                ],\n                                startPoint: .top, endPoint: .bottom\n                            )\n                        }\n                        .padding(screenPadding ? 2 + generalPadding : 2)\n                        .padding(.top, geometry.size.height / 15 - 2)\n                }\n        }\n    }\n    \n    @ViewBuilder\n    func notchView(geometry: GeometryProxy) -> some View {\n        if UIDevice.frameType == .dynamicIsland {\n            Capsule()\n                .fill(accentColor)\n                .frame(height: geometry.size.height / 25)\n                .padding(.horizontal, geometry.size.width / 2.8)\n                .padding(.top, 2 * scale)\n        } else {\n            UnevenRoundedRectangle(\n                cornerRadii: .init(\n                    topLeading: 0,\n                    bottomLeading: geometry.size.width * frameCornerRadiusScaleFactor / 5,\n                    bottomTrailing: geometry.size.width * frameCornerRadiusScaleFactor / 5,\n                    topTrailing: 0\n                )\n            )\n            .fill(accentColor)\n            .frame(height: geometry.size.height / 15 - 2)\n            .padding(.horizontal, geometry.size.width / (UIDevice.frameType == .narrowNotch ? 3 : 4))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Components/SettingsHeaderView.swift",
    "content": "//\n//  SettingsHeaderView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-17.\n//\n\nimport Icons\nimport SwiftUI\n\nstruct SettingsHeaderView<IconView: View>: View {\n    let title: String\n    let description: String?\n    let icon: IconView\n        \n    init(\n        title: LocalizedStringResource,\n        description: LocalizedStringResource?,\n        @ViewBuilder icon: @escaping () -> IconView\n    ) {\n        self.title = .init(localized: title)\n        if let description {\n            self.description = .init(localized: description)\n        } else {\n            self.description = nil\n        }\n        self.icon = icon()\n    }\n    \n    var body: some View {\n        Section {\n            VStack(alignment: .leading, spacing: 4) {\n                icon\n                    .symbolVariant(.fill)\n                    .padding(.bottom, 11)\n                Text(title)\n                    .font(.title2)\n                    .fontWeight(.bold)\n                if let description {\n                    Text(description)\n                        .foregroundStyle(.themedSecondary)\n                }\n            }\n            .frame(maxWidth: .infinity, alignment: .leading)\n        }\n    }\n}\n\nstruct SettingsHeaderIconView: View {\n    let icon: Icon\n    \n    var body: some View {\n        Image(icon: icon)\n            .font(.title)\n            .symbolVariant(.fill)\n            .imageScale(.large)\n            .foregroundStyle(.themedContrastingLabel)\n            .frame(width: 60, height: 60)\n            .background(.tint, in: .rect(cornerRadius: 15))\n    }\n}\n\n// - MARK: Alternative Initializers\n\nextension SettingsHeaderView {\n    init(\n        title: LocalizedStringResource,\n        description: LocalizedStringResource?,\n        icon: Icon\n    ) where IconView == SettingsHeaderIconView {\n        self.init(title: title, description: description) {\n            SettingsHeaderIconView(icon: icon)\n        }\n    }\n    \n    @_disfavoredOverload\n    init(\n        title: LocalizedStringResource,\n        description: some StringProtocol,\n        icon: Icon\n    ) where IconView == SettingsHeaderIconView {\n        self.title = .init(localized: title)\n        self.description = String(description)\n        self.icon = SettingsHeaderIconView(icon: icon)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Components/SettingsInteractionBarSummaryView.swift",
    "content": "//\n//  SettingsInteractionBarSummaryView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-18.\n//\n\nimport SwiftUI\n\nstruct SettingsInteractionBarSummaryView<Configuration: InteractionBarConfiguration>: View {\n    var title: LocalizedStringResource = \"Interaction Bar\"\n    var configuration: Configuration\n    \n    var body: some View {\n        HStack(spacing: 4) {\n            Text(title)\n                .frame(maxWidth: .infinity, alignment: .leading)\n            ForEach(configuration.all, id: \\.self) { item in\n                HStack(spacing: 0) {\n                    switch item {\n                    case let .action(action):\n                        Image(systemName: action.appearance.barIcon)\n                            .frame(width: 24, height: 24)\n                    case let .counter(counter):\n                        if let appearance = counter.appearance.leading {\n                            Image(systemName: appearance.barIcon)\n                                .frame(width: 24, height: 24)\n                        }\n                        if let appearance = counter.appearance.trailing {\n                            Image(systemName: appearance.barIcon)\n                                .frame(width: 24, height: 24)\n                        }\n                    }\n                }\n                .font(.footnote)\n                .fontDesign(.rounded)\n                .fontWeight(.semibold)\n                .background(.themedTertiaryGroupedBackground, in: .rect(cornerRadius: 5))\n            }\n            .foregroundStyle(.themedSecondary)\n            .lineLimit(1)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Components/SquircleLabelStyle.swift",
    "content": "//\n//  SquircleLabelStyle.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2024.\n//\n\nimport SwiftUI\n\nstruct SquircleLabelStyle: LabelStyle {\n    func makeBody(configuration: Configuration) -> some View {\n        HStack(alignment: .center, spacing: 16) {\n            configuration.icon\n                .font(.body)\n                .symbolVariant(.fill)\n                .foregroundStyle(.themedContrastingLabel)\n                .frame(width: Constants.main.settingsIconSize, height: Constants.main.settingsIconSize)\n                .background(.tint)\n                .clipShape(.rect(cornerRadius: Constants.main.smallItemCornerRadius))\n                .accessibilityHidden(true)\n            configuration.title\n        }\n    }\n}\n\nextension LabelStyle where Self == SquircleLabelStyle {\n    static var squircle: SquircleLabelStyle { .init() }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Components/ThemeLabel.swift",
    "content": "//\n//  ThemeLabel.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2024.\n//\n\nimport SwiftUI\n\nstruct ThemeLabel: View {\n    var title: LocalizedStringResource\n    var color1: Color\n    var color2: Color?\n    var outlineColor: Color = .secondary\n    \n    var body: some View {\n        Label {\n            Text(title)\n        } icon: {\n            if let color2 {\n                color1\n                    .frame(width: Constants.main.settingsIconSize, height: Constants.main.settingsIconSize)\n                    .overlay { ThemeTriangle().fill(color2) }\n                    .clipShape(RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius))\n                    .overlay {\n                        RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius)\n                            .stroke(outlineColor, lineWidth: 1)\n                    }\n                \n            } else {\n                color1\n                    .frame(width: Constants.main.settingsIconSize, height: Constants.main.settingsIconSize)\n                    .clipShape(RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius))\n                    .overlay {\n                        RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius)\n                            .stroke(outlineColor, lineWidth: 1)\n                    }\n            }\n        }\n    }\n}\n\nextension ThemeLabel {\n    init(title: LocalizedStringResource? = nil, palette: PaletteOption) {\n        self.init(\n            title: title ?? palette.label,\n            color1: palette.palette.accent,\n            color2: palette.palette.background.primary\n        )\n    }\n}\n\nprivate struct ThemeTriangle: Shape {\n    func path(in rect: CGRect) -> Path {\n        var path = Path()\n\n        path.move(to: CGPoint(x: rect.minX, y: rect.maxY))\n        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))\n        path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))\n        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))\n\n        return path\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ContextMenuSettingsView.swift",
    "content": "//\n//  ContextMenuSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-02-22.\n//\n\nimport ComponentViews\nimport Actions\nimport SwiftUI\n\nstruct ContextMenuSettingsView<Configuration: ContextMenuConfiguration>: View {\n    @Binding var configuration: [ActionSeed]\n\n    var body: some View {\n        Form {\n            ForEach(configuration, id: \\.key) { seed in\n                Label(seed.label)\n                    .foregroundStyle(seed.label.isDestructive ? .themedWarning : .themedPrimary)\n            }\n            .onMove { fromOffsets, toOffset in\n                configuration.move(fromOffsets: fromOffsets, toOffset: toOffset)\n            }\n            .onDelete { offsets in\n                configuration.remove(atOffsets: offsets)\n            }\n            ForEach(Array(Configuration.availableActions.sections.enumerated()), id: \\.offset) { _, seeds in\n                drawerActionSectionView(seeds)\n            }\n        }\n        .toolbar {\n            CloseButtonToolbarItem(ios18Label: .xmark)\n        }\n        .navigationTitle(\"Customize Context Menu\")\n        .navigationBarTitleDisplayMode(.inline)\n        .environment(\\.editMode, .constant(.active))\n    }\n\n    @ViewBuilder\n    func drawerActionSectionView(_ seeds: [ActionSeed]) -> some View {\n        Section {\n            ForEach(seeds, id: \\.key, content: drawerActionRowView)\n        }\n    }\n\n    @ViewBuilder\n    func drawerActionRowView(_ seed: ActionSeed) -> some View {\n        Button {\n            withAnimation {\n                configuration.append(seed)\n            }\n        } label: {\n            HStack {\n                Label(seed.label)\n                    .foregroundStyle(seed.label.isDestructive ? .themedWarning : .themedPrimary)\n                Spacer()\n                if !configuration.contains(seed) {\n                    Image(icon: .general.add)\n                        .symbolVariant(.circle.fill)\n                        .foregroundStyle(.themedAccent)\n                        .imageScale(.large)\n                }\n            }\n        }\n        .buttonStyle(.plain)\n        .disabled(configuration.contains(seed))\n    }\n}\n\nextension ContextMenuSettingsView {\n    init(_ keyPath: ReferenceWritableKeyPath<SettingsValues, Configuration>) {\n        self.init(configuration: .init(get: {\n            Settings.get(keyPath).contextMenu\n        }, set: { newValue in\n            Settings.mutate(keyPath) { $0.contextMenu = newValue }\n        }))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/DefaultFeedSettingsView.swift",
    "content": "//\n//  DefaultFeedSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-31.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct DefaultFeedSettingsView: View {\n    @Setting(\\.feed_default) var defaultFeed\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Default Feed\",\n                description: \"Choose which feed is shown when the app opens.\"\n            ) {}\n            Picker(\"Default Feed\", selection: $defaultFeed) {\n                ForEach(ListingType.allCases, id: \\.self) { item in\n                    Label {\n                        Text(item.description.label)\n                    } icon: {\n                        FeedIconView(feedDescription: item.description, size: 30)\n                    }\n                }\n            }\n            .pickerStyle(.inline)\n            .labelsHidden()\n        }\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Default Feed\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/DeveloperSettingsView.swift",
    "content": "//\n//  DeveloperSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 22/09/2024.\n//\n\nimport Dependencies\nimport FediverseEvents\nimport MlemBackend\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\n// Strings in this view are intentionally left unlocalized; we shouldn't\n// be burdening translators with these when they'll never be used\n\nstruct DeveloperSettingsView: View {\n    @Environment(BackendClient.self) var backendClient\n    @Environment(EventsTracker.self) var eventsTracker\n    \n    @Environment(NavigationLayer.self) var navigation\n    @Dependency(\\.persistenceRepository) var persistenceRepository\n    \n    @Setting(\\.tip_feedWelcomePrompt) var showFeedWelcomePrompt\n    @Setting(\\.dev_developerMode) var developerMode\n    @Setting(\\.dev_errorTimeout) var errorToastTimeout\n    \n    @AppStorage(\"lastTestFlightUpdate\") var lastTestFlightUpdate: URL?\n    \n    @State var backendStatus: BackendHealthCheck?\n    @State var lastBackendStatusCheck: Date?\n    \n    var body: some View {\n        Form {\n            Section {\n                Toggle(String(\"Developer Mode\"), isOn: $developerMode)\n                NavigationLink(String(\"Error Log\"), destination: .settings(.errorLog))\n            }\n            \n            errorToastTimeoutSection\n\n            Section {\n                if let backendStatus {\n                    if backendStatus.unhealthyReasons.isEmpty {\n                        backendStatusRow(isHealthy: true)\n                    } else {\n                        backendStatusRow(isHealthy: false)\n                        \n                        ForEach(Array(backendStatus.unhealthyReasons.enumerated()), id: \\.offset) { _, reason in\n                            Text(reason)\n                                .padding(.leading, Constants.main.standardSpacing)\n                                .foregroundStyle(.themedNegative)\n                        }\n                    }\n                } else {\n                    backendStatusRow(isHealthy: nil)\n                }\n                \n                Button(\"Refresh\") { checkBackendStatus() }\n            } header: {\n                Text(verbatim: \"Backend\")\n            } footer: {\n                if let lastBackendStatusCheck {\n                    Text(verbatim: \"Refreshed \\(lastBackendStatusCheck.formatted(date: .abbreviated, time: .standard))\")\n                } else {\n                    Text(verbatim: \"Refreshing...\")\n                }\n            }\n            .onAppear { checkBackendStatus() }\n            \n            #if DEBUG\n                Section {\n                    Toggle(String(\"Use QC Mlem Backend\"),\n                           isOn: .init(get: { backendClient.environment == .qualityControl },\n                                       set: { backendClient.changeEnvironment(to: $0 ? .qualityControl : .production) }))\n                    \n                    Toggle(String(\"Use QC Events API\"),\n                           isOn: .init(get: { eventsTracker.environment == .qualityControl },\n                                       set: { eventsTracker.changeEnvironment(to: $0 ? .qualityControl : .production) }))\n                } footer: {\n                    Text(verbatim: \"These settings will be cleared when the app restarts.\")\n                }\n\n                Section {\n                    Button(String(\"Trigger Onboarding\")) {\n                        navigation.showFullScreenCover(.onboarding)\n                    }\n                    \n                    Button(String(\"Reset Feed Welcome Banner\")) {\n                        showFeedWelcomePrompt = true\n                    }\n                    \n                    Button(String(\"Reset Feed TestFlight Banner\")) {\n                        lastTestFlightUpdate = nil\n                    }\n                \n                    Button(String(\"Create Error\")) {\n                        handleError(ApiClientError.insufficientPermissions)\n                    }\n                \n                    Button(String(\"Create Silent Error\")) {\n                        handleError(ApiClientError.noEntityFound, silent: true)\n                    }\n                } header: {\n                    Text(verbatim: \"Debug Tools\")\n                }\n            #endif\n            Button(String(\"Reset Settings State\")) {\n                do {\n                    try persistenceRepository.deleteAllSystemSettings()\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n        .navigationTitle(\"Developer\")\n    }\n\n    @ViewBuilder\n    private var errorToastTimeoutSection: some View {\n        Section {\n            HStack {\n                Text(String(\"Error Toast Timeout\"))\n                Spacer()\n\n                Group {\n                    if errorToastTimeout == 100_000 {\n                        Image(systemName: \"infinity\")\n                    } else {\n                        Text(String(format: \"%.1f\", errorToastTimeout) + \"s\")\n                    }\n                }\n                .foregroundStyle(.themedSecondary)\n            }\n            Slider(\n                value: .init(\n                    get: { errorToastTimeout == 100_000 ? 10 : errorToastTimeout },\n                    set: { errorToastTimeout = ($0 == 10 ? 100_000 : $0) }\n                ),\n                in: 0.5...10\n            )\n        } footer: {\n            Text(String(\"Default: 1.5s\"))\n        }\n    }\n    \n    @ViewBuilder\n    private func backendStatusRow(isHealthy: Bool?) -> some View {\n        HStack {\n            Text(verbatim: \"Status\")\n            Spacer()\n            if let isHealthy {\n                Image(icon: .general.circle)\n                    .foregroundStyle(isHealthy ? .themedPositive : .themedNegative)\n                    .symbolVariant(.fill)\n            } else {\n                ProgressView()\n            }\n        }\n    }\n    \n    private func checkBackendStatus() {\n        Task {\n            do {\n                backendStatus = try await backendClient.healthCheck()\n            } catch {\n                handleError(error)\n                backendStatus = nil\n            }\n            lastBackendStatusCheck = .now\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Discussion Languages/DiscussionLanguageSettingsView.swift",
    "content": "//\n//  DiscussionLanguageSettingsView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-02-06.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct DiscussionLanguageSettingsView: View {\n    @Environment(NavigationLayer.self) var navigation\n    \n    @State var instance: Instance?\n    @State var person: Person?\n    \n    @State var submitting: Int?\n    \n    init() {\n        if let firstInstance = AppState.main.firstApi.myInstance {\n            self._instance = .init(wrappedValue: firstInstance)\n        }\n        if let firstPerson = AppState.main.firstPerson {\n            self._person = .init(wrappedValue: firstPerson)\n        }\n    }\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Discussion Languages\",\n                description: \"Choose which languages appear in your feed. Posts and comments in other languages will be hidden.\",\n                icon: .settings.language\n            )\n            \n            if let person, let instance {\n                ExpectedView(person.discussionLanguageIds) { languageIds in\n                    Section {\n                        let selectedLanguages = instance.languages(withIds: languageIds)\n                        ForEach(selectedLanguages, id: \\.languageCode) { language in\n                            LanguageListRowBody(language: language)\n                                .contextMenu {\n                                    Button(\"Remove\", icon: .general.signOut, role: .destructive) {\n                                        Task { await updateDiscussionLanguages(with: language, languages: languageIds) }\n                                    }\n                                }\n                                .swipeActions(edge: .trailing, allowsFullSwipe: false) {\n                                    Button(\"Remove\", role: .destructive) {\n                                        Task { await updateDiscussionLanguages(with: language, languages: languageIds) }\n                                    }\n                                    .buttonStyle(.automatic)\n                                    .tint(.red)\n                                }\n                        }\n                        Button(\"Add Language...\") {\n                            navigation.openSheet(.languagePicker(selectedLanguages: Set(selectedLanguages)) { newLanguage in\n                                Task { await updateDiscussionLanguages(with: newLanguage, languages: languageIds) }\n                            })\n                        }\n                    }\n                }\n            }\n        }\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Discussion Languages\")\n    }\n\n    func updateDiscussionLanguages(with language: Locale.Language, languages: Set<Int>) async {\n        defer { submitting = nil }\n\n        guard let person, let instance else {\n            assertionFailure()\n            return\n        }\n\n        guard let id = instance.getLanguageId(for: language) else {\n            assertionFailure()\n            return\n        }\n        \n        guard let updateSettings = person.updateSettings else {\n            assertionFailure()\n            return\n        }\n        \n        var newLangs = languages\n        if newLangs.contains(id) {\n            newLangs.remove(id)\n        } else {\n            newLangs.insert(id)\n        }\n        do {\n            try await updateSettings(.init(discussionLanguageIds: newLangs))\n        } catch {\n            handleError(error)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Discussion Languages/LanguageListRowBody.swift",
    "content": "//\n//  LanguageListRowBody.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-01.\n//\n\nimport SwiftUI\n\nstruct LanguageListRowBody: View {\n    @Environment(\\.locale) private var userLocale\n    \n    let language: Locale.Language\n    \n    var body: some View {\n        let code = language.languageCode?.identifier ?? \"\"\n        let locale = Locale(languageCode: language.languageCode)\n        VStack(alignment: .leading) {\n            Text(locale.localizedString(forLanguageCode: code)?.capitalized ?? \"\")\n            Text(userLocale.localizedString(forLanguageCode: code) ?? \"\")\n                .font(.footnote)\n                .foregroundStyle(.secondary)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Discussion Languages/LanguagePickerSheetView.swift",
    "content": "//\n//  LanguagePickerSheetView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-28.\n//\n\nimport SwiftUI\n\nstruct LanguagePickerSheetView: View {\n    @Environment(AppState.self) var appState\n    @Environment(\\.dismiss) var dismiss\n    @Environment(\\.locale) private var userLocale\n\n    let selectedLanguages: Set<Locale.Language>\n    let callback: (Locale.Language) -> Void\n    \n    @State var query: String = \"\"\n\n    var allLanguages: [Locale.Language] {\n        appState.firstSession.instance?.allLanguages.value ?? []\n    }\n    \n    var suggestedLanguages: [Locale.Language] {\n        let deviceLanguageCodes = Locale.preferredLanguages.compactMap {\n            $0.split(separator: \"-\").first\n        }.map(String.init).uniqued()\n        \n        var validDeviceLanguages: Set<String> = .init()\n        for language in allLanguages {\n            let code = language.languageCode?.identifier ?? \"\"\n            if deviceLanguageCodes.contains(code) {\n                validDeviceLanguages.insert(code)\n            }\n        }\n        \n        return deviceLanguageCodes\n            .filter { validDeviceLanguages.contains($0) }\n            .map(Locale.Language.init)\n            .filter { !selectedLanguages.contains($0) }\n    }\n    \n    var searchResults: [Locale.Language] {\n        allLanguages.filter { language in\n            let code = language.languageCode?.identifier ?? \"\"\n            let locale = Locale(languageCode: language.languageCode)\n            \n            if let name = locale.localizedString(forLanguageCode: code) {\n                if name.localizedCaseInsensitiveContains(query) {\n                    return true\n                }\n            }\n            \n            if let name = userLocale.localizedString(forLanguageCode: code) {\n                if name.localizedCaseInsensitiveContains(query) {\n                    return true\n                }\n            }\n            \n            return false\n        }\n    }\n    \n    var body: some View {\n        Form {\n            if query.isEmpty {\n                if !suggestedLanguages.isEmpty {\n                    Section(\"Suggested Languages\") {\n                        ForEach(suggestedLanguages, id: \\.languageCode, content: languageRow)\n                    }\n                }\n                Section(\"All Languages\") {\n                    ForEach(allLanguages, id: \\.languageCode, content: languageRow)\n                }\n            } else {\n                Section {\n                    ForEach(searchResults, id: \\.languageCode, content: languageRow)\n                }\n            }\n        }\n        .contentMargins(.top, searchResults.isEmpty ? nil : 16)\n        .navigationTitle(\"Choose Language\")\n        .navigationBarTitleDisplayMode(.inline)\n        .withSheetSearch(query: $query)\n    }\n    \n    func languageRow(_ language: Locale.Language) -> some View {\n        LanguageListRowBody(language: language)\n            .frame(maxWidth: .infinity, alignment: .leading)\n            .contentShape(.rect)\n            .onTapGesture {\n                callback(language)\n                dismiss()\n            }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/DiscussionLanguageSettingsView.swift",
    "content": "//\n//  DiscussionLanguageSettingsView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-02-06.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct DiscussionLanguageSettingsView: View {\n    @State var instance: (any Instance3Providing)?\n    @State var person: (any Person4Providing)?\n    \n    @State var submitting: Int?\n    \n    init() {\n        if let firstInstance = AppState.main.firstApi.myInstance {\n            self._instance = .init(wrappedValue: firstInstance)\n        }\n        if let firstPerson = AppState.main.firstPerson {\n            self._person = .init(wrappedValue: firstPerson)\n        }\n    }\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Discussion Languages\",\n                description: \"Choose which languages appear in your feed. Posts and comments in other languages will be hidden.\",\n                systemImage: Icons.language\n            )\n            \n            if let languages = person?.discussionLanguages, !languages.contains(0) {\n                Section {\n                    Label(\"You will not see most content if Undetermined is not selected.\", systemImage: Icons.warningFill)\n                        .foregroundStyle(.themedWarning)\n                }\n            }\n            \n            Section {\n                if let instance, let person {\n                    ForEach(instance.allLanguages, id: \\.id) { language in\n                        Button {\n                            if submitting == nil {\n                                submitting = language.id\n                                Task {\n                                    await updateDiscussionLanguages(with: language.id)\n                                }\n                            }\n                        } label: {\n                            HStack {\n                                Text(language.name)\n                                \n                                Spacer()\n                                \n                                if submitting == language.id {\n                                    ProgressView()\n                                } else if person.discussionLanguages.contains(language.id) {\n                                    Image(systemName: Icons.success)\n                                        .foregroundStyle(.themedAccent)\n                                }\n                            }\n                            .contentShape(.rect)\n                        }\n                        .buttonStyle(.plain)\n                    }\n                } else {\n                    ProgressView()\n                        .task {\n                            do {\n                                try await (person, instance, _) = AppState.main.firstApi.getMyPerson()\n                            } catch {\n                                handleError(error)\n                            }\n                        }\n                }\n            }\n        }\n        .contentMargins(.top, 16)\n    }\n    \n    func updateDiscussionLanguages(with id: Int) async {\n        defer { submitting = nil }\n        \n        guard let person else {\n            assertionFailure(\"No person found\")\n            return\n        }\n        \n        var newLangs = person.discussionLanguages\n        if newLangs.contains(id) {\n            newLangs.remove(id)\n        } else {\n            newLangs.insert(id)\n        }\n        do {\n            try await person.person4.updateSettings(discussionLanguages: newLangs)\n        } catch {\n            handleError(error)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/EmbeddingSettingsView.swift",
    "content": "//\n//  EmbeddingsSettingsView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-01-22.\n//\n\nimport SwiftUI\n\nstruct EmbeddingSettingsView: View {\n    @Setting(\\.links_embedLoops) var embedLoops\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Embedded Content\",\n                description: \"Display linked media from supported hosts in-app rather than as a link.\",\n                icon: .general.embedding\n            )\n            // TODO: use loops.video logo directly (hence why this is not in Icons)\n            Toggle(String(\"loops.video\"), systemImage: \"repeat\", isOn: $embedLoops)\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Embedded Content\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ErrorLogView.swift",
    "content": "//\n//  ErrorLogView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-12-29.\n//\n\nimport Rest\nimport SwiftUI\nimport Theming\n\nstruct ErrorLogView: View {\n    @Environment(ErrorsTracker.self) var errorsTracker\n    @Environment(NavigationLayer.self) var navigation\n    \n    var body: some View {\n        FancyScrollView {\n            LazyVStack(spacing: Constants.main.standardSpacing) {\n                if errorsTracker.errors.isEmpty {\n                    Text(verbatim: \"No errors\")\n                        .foregroundStyle(.themedSecondary)\n                }\n                ForEach(Array(errorsTracker.errors.enumerated()), id: \\.offset) { _, errorDetails in\n                    errorView(errorDetails)\n                }\n                .padding(.horizontal, Constants.main.standardSpacing)\n            }\n        }\n        .themedGroupedBackground()\n        .navigationTitle(String(\"Error Log\"))\n        .toolbar {\n            if !errorsTracker.errors.isEmpty {\n                ToolbarItem(placement: .topBarTrailing) {\n                    Button {\n                        Task {\n                            if let url = await downloadTextToFileSystem(\n                                fileName: \"mlem_error_log.txt\",\n                                text: errorsTracker.createErrorLog()\n                            ) {\n                                navigation.model?.shareInfo = .init(url: url)\n                            } else {\n                                ToastModel.main.add(.failure(String(\"Failed to share error log\")))\n                            }\n                        }\n                    } label: {\n                        Image(icon: .general.share)\n                    }\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func errorView(_ details: ErrorDetails) -> some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            HStack {\n                Text(details.title ?? \"Error\")\n                    .fontWeight(.semibold)\n                \n                Spacer()\n                \n                Button {\n                    UIPasteboard.general.string = details.errorText()\n                    ToastModel.main.add(.success(String(\"Copied\")))\n                } label: {\n                    Text(Image(icon: .general.copy))\n                        .font(.subheadline)\n                        .foregroundStyle(.themedAccent)\n                }\n            }\n            \n            Text(details.errorText(includingLocation: false))\n                .font(.caption)\n                .monospaced()\n            \n            if let location = details.location {\n                HStack(alignment: .top, spacing: 2) {\n                    Image(systemName: \"arrow.turn.down.right\")\n                        .offset(y: 2)\n                    \n                    Text(location)\n                        .monospaced()\n                }\n                .font(.caption)\n            }\n            \n            Text(details.when.formatted(date: .abbreviated, time: .standard))\n                .font(.caption)\n                .foregroundStyle(.themedSecondary)\n        }\n        .padding(Constants.main.standardSpacing)\n        .background(.themedSecondaryGroupedBackground)\n        .clipShape(.rect(cornerRadius: Constants.main.mediumItemCornerRadius))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ExternalLinkSettingsView.swift",
    "content": "//\n//  ExternalLinkSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-28.\n//\n\nimport SwiftUI\n\nstruct ExternalLinkSettingsView: View {\n    @Setting(\\.links_openInBrowser) var openLinksInBrowser\n    @Setting(\\.links_readerMode) var openLinksInReaderMode\n    \n    var body: some View {\n        Form {\n            Section(\"Open External Links\") {\n                Picker(\"Open External Links\", selection: $openLinksInBrowser) {\n                    Label(\"In Mlem\", icon: .settings.inApp).tag(false)\n                    Label(\"In Default Browser\", icon: .general.browser).tag(true)\n                }\n                .pickerStyle(.inline)\n                .labelsHidden()\n            }\n            \n            Section {\n                Toggle(\"Open in Reader\", icon: .settings.reader, isOn: $openLinksInReaderMode)\n                    .disabled(openLinksInBrowser)\n            } footer: {\n                Text(\"Automatically enable Reader for supported webpages. You can only enable this when using the in-app browser.\")\n            }\n        }\n        .navigationTitle(\"External Links\")\n        .withConditionalLabelStyle()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/FiltersSettingsView.swift",
    "content": "//\n//  FiltersSettingsView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-12-22.\n//\n\nimport Dependencies\nimport SwiftUI\nimport os\n\nstruct FiltersSettingsView: View {\n    @Setting(\\.filters_keywordFilterEnabled) var keywordFilterEnabled\n    @Setting(\\.filters_literalFilterEnabled) var literalFilterEnabled\n    \n    @Environment(FiltersTracker.self) var filtersTracker\n    @Environment(NavigationLayer.self) var navigation\n    \n    @State var newKeyword: String = \"\"\n    @State var newLiteral: String = \"\"\n    \n    @State var legacyWarningDisplayed: Bool = false\n    \n    var headerDescription: String {\n        var output = String(localized: \"Hide posts containing certain words, phrases, or character sequences from your feed.\")\n        if AccountsTracker.main.highestLevelAccountType >= .moderator {\n            output += \" \"\n            // swiftlint:disable:next line_length\n            output += String(localized: \"If you are a moderator or administrator of a filtered post, it will appear in your feed but require you to tap to view its content.\")\n        }\n        return output\n    }\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Filters\",\n                description: headerDescription,\n                icon: .settings.keywordFilter\n            )\n            \n            Section(\"Keywords\") {\n                keywordSection\n            } footer: {\n                // swiftlint:disable:next line_length\n                Text(\"Hide posts with titles containing these whole words or phrases. Ignores case and punctuation (e.g., the keyword \\\"john\\\" will also filter \\\"John's\\\").\")\n            }\n            \n            Section(\"Literals\") {\n                literalSection\n            } footer: {\n                Text(\"Hide posts with titles containing containing these precise character sequences.\")\n            }\n        }\n        .scrollDismissesKeyboard(.interactively)\n        .withConditionalLabelStyle()\n        .navigationTitle(\"Filters\")\n        .versionAwareDialog(\"Deprecated Format\", isPresented: $legacyWarningDisplayed) {\n            Button(\"Re-Export\") { export() }\n            Button(\"Close\", role: .cancel) {}\n        } message: {\n            // swiftlint:disable:next line_length\n            Text(\"These filters were saved by an older version of Mlem, and will not be compatible with future versions. To preserve compatibility, re-export your filters.\")\n        }\n        .toolbar {\n            ToolbarItem(placement: .topBarTrailing) {\n                Menu(\"More...\", icon: .general.toolbarMenu) {\n                    Button(\"Export...\", icon: .general.export) { export() }\n                    Button(\"Import...\", icon: .general.import) {\n                        navigation.showFilePicker(types: [.plainText, .json]) { data in\n                            do {\n                                let jsonData = try JSONDecoder().decode(ExportableFilters.self, from: data)\n                                Task { @MainActor in\n                                    await filtersTracker.resetFilteredKeywords(to: jsonData.rawKeywords)\n                                    filtersTracker.resetFilteredLiterals(to: jsonData.literals)\n                                }\n                            } catch {\n                                // TODO: Mlem 2.5 remove legacy compatibility\n                                let text = String(data: data, encoding: .utf8) ?? \"\"\n                                await filtersTracker.resetFilteredKeywords(\n                                    to: Set(text.split(separator: \"\\n\").map(String.init))\n                                )\n                                legacyWarningDisplayed = true\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var keywordSection: some View {\n        Toggle(\"Enable\", icon: .settings.keywordFilter, isOn: $keywordFilterEnabled)\n        \n        TextField(\"New Keyword...\", text: $newKeyword)\n            .textCase(.lowercase)\n            .textInputAutocapitalization(.never)\n            .submitLabel(.done)\n            .onSubmit {\n                saveNewKeyword()\n            }\n        \n        ForEach(filtersTracker.rawKeywords.sorted(by: <), id: \\.self) { keyword in\n            HStack {\n                Text(keyword)\n                \n                Spacer()\n                \n                // using a Button to do this makes the whole row register tap gestures :/\n                Image(icon: .general.delete)\n                    .foregroundStyle(.themedWarning)\n                    .onTapGesture {\n                        deleteKeyword(keyword)\n                    }\n            }\n        }\n    }\n    \n    func saveNewKeyword() {\n        guard !newKeyword.isEmpty else { return }\n        \n        let cleanedKeyword = newKeyword.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)\n        \n        Task {\n            await filtersTracker.addFilteredKeyword(cleanedKeyword)\n            newKeyword = \"\"\n        }\n    }\n    \n    func deleteKeyword(_ keyword: String) {\n        guard filtersTracker.rawKeywords.contains(keyword) else {\n            return\n        }\n        \n        Task {\n            await filtersTracker.removeFilteredKeyword(keyword)\n        }\n    }\n    \n    @ViewBuilder\n    var literalSection: some View {\n        Toggle(\"Enable\", icon: .settings.keywordFilter, isOn: $literalFilterEnabled)\n        \n        TextField(\"New Literal...\", text: $newLiteral)\n            .textCase(.lowercase)\n            .textInputAutocapitalization(.never)\n            .submitLabel(.done)\n            .onSubmit {\n                saveNewLiteral()\n            }\n        \n        ForEach(filtersTracker.literals.sorted(by: <), id: \\.self) { literal in\n            HStack {\n                Text(literal)\n                \n                Spacer()\n                \n                // using a Button to do this makes the whole row register tap gestures :/\n                Image(icon: .general.delete)\n                    .foregroundStyle(.themedWarning)\n                    .onTapGesture {\n                        deleteLiteral(literal)\n                    }\n            }\n        }\n    }\n    \n    func saveNewLiteral() {\n        guard !newLiteral.isEmpty else { return }\n        \n        Task {\n            await filtersTracker.addFilteredLiteral(newLiteral)\n            newLiteral = \"\"\n        }\n    }\n    \n    func deleteLiteral(_ literal: String) {\n        guard filtersTracker.literals.contains(literal) else {\n            return\n        }\n        \n        Task {\n            await filtersTracker.removeFilteredLiteral(literal)\n        }\n    }\n    \n    func export() {\n        do {\n            let jsonData = try JSONEncoder().encode(ExportableFilters(\n                rawKeywords: filtersTracker.rawKeywords,\n                literals: filtersTracker.literals))\n            \n            Task {\n                if let jsonString = String(data: jsonData, encoding: .utf8), let url = await downloadTextToFileSystem(\n                    fileName: \"filters.json\",\n                    text: jsonString) {\n                    navigation.model?.shareInfo = .init(url: url)\n                } else {\n                    ToastModel.main.add(.failure())\n                }\n            }\n        } catch {\n            handleError(error)\n        }\n    }\n}\n\nprivate struct ExportableFilters: Codable {\n    let rawKeywords: Set<String>\n    let literals: Set<String>\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/GeneralSettingsView.swift",
    "content": "//\n//  GeneralSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2024.\n//\n\nimport Dependencies\nimport SwiftUI\nimport Theming\n\nstruct GeneralSettingsView: View {\n    // behavior\n    @Setting(\\.behavior_upvoteOnSave) var upvoteOnSave\n    @Setting(\\.feed_markReadOnScroll) var markReadOnScroll\n    @Setting(\\.behavior_infiniteScroll) var infiniteScroll\n    @Setting(\\.feed_default) var defaultFeed\n    @Setting(\\.behavior_hapticLevel) var hapticLevel\n    @Setting(\\.markdown_wrapCodeBlockLines) var wrapCodeBlockLines\n    @Setting(\\.person_ageVisibility) var accountAgeVisibility\n    @Setting(\\.media_animatedAvatars) var animatedAvatars\n    @Setting(\\.events_showEvents) var showEvents\n\n    // gestures\n    @Setting(\\.behavior_enableQuickSwipes) var swipeActionsEnabled\n    @Setting(\\.navigation_swipeAnywhere) var swipeAnywhereToNavigate\n    \n    // avatars\n    @Setting(\\.person_showAvatar) var showPersonAvatar\n    @Setting(\\.community_showAvatar) var showCommunityAvatar\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"General\",\n                description: \"Manage your overall setup for Mlem.\",\n                icon: .settings.general\n            )\n            .gradientTint(.themedNeutralAccent)\n            Section {\n                NavigationLink(\n                    \"Default Feed\",\n                    value: .init(localized: defaultFeed.label),\n                    fallbackValue: \"\",\n                    icon: .lemmy.feed,\n                    destination: .settings(.defaultFeed)\n                )\n                NavigationLink(\n                    \"Haptics\",\n                    value: .init(localized: hapticLevel?.label ?? \"None\"),\n                    fallbackValue: \"\",\n                    icon: .general.haptics,\n                    destination: .settings(.haptics)\n                )\n            }\n            Section {\n                Toggle(\"Upvote on Save\", icon: .settings.upvoteOnSave, isOn: $upvoteOnSave)\n                Toggle(\"Mark Read on Scroll\", icon: .settings.markReadOnScroll, isOn: $markReadOnScroll)\n                Toggle(\"Infinite Scroll\", icon: .settings.infiniteScroll, isOn: $infiniteScroll)\n                Toggle(\"Wrap Code Block Lines\", icon: .markdown.inlineCode, isOn: $wrapCodeBlockLines)\n            }\n            Section {\n                Toggle(\n                    \"Swipe Actions\",\n                    icon: .settings.swipeActions,\n                    isOn: .init(\n                        get: { swipeActionsEnabled },\n                        set: {\n                            swipeActionsEnabled = $0\n                            if $0 {\n                                swipeAnywhereToNavigate = false\n                            }\n                        }\n                    )\n                )\n                if !UIDevice.isIos26 {\n                    Toggle(\n                        \"Swipe Anywhere to Navigate\",\n                        icon: .settings.swipeAnywhere,\n                        isOn: .init(\n                            get: { swipeAnywhereToNavigate },\n                            set: {\n                                swipeAnywhereToNavigate = $0\n                                if $0 {\n                                    swipeActionsEnabled = false\n                                }\n                            }\n                        )\n                    )\n                }\n            }\n            \n            Section {\n                NavigationLink(\n                    \"Show Account Age\",\n                    value: .init(localized: accountAgeVisibility.label),\n                    fallbackValue: \"\",\n                    icon: .lemmy.newAccountFlair,\n                    destination: .settings(.accountAgeVisibility)\n                )\n            }\n            \n            Section {\n                Toggle(\"User Avatar\", icon: .lemmy.person, isOn: $showPersonAvatar)\n                    .symbolVariant(.circle)\n                Toggle(\"Community Avatar\", icon: .lemmy.community, isOn: $showCommunityAvatar)\n                    .symbolVariant(.circle)\n                if #available(iOS 18, *) {\n                    NavigationLink(\n                        \"Animated Avatars\",\n                        value: .init(localized: animatedAvatars.label),\n                        fallbackValue: \"\",\n                        icon: .general.playCircle,\n                        destination: .settings(.animatedAvatars)\n                    )\n                }\n            }\n\n            Section {\n                Toggle(\"Show Events\", icon: .lemmy.event, isOn: $showEvents)\n            }\n            \n            NavigationLink(\n                \"Import/Export Settings\",\n                icon: .general.import,\n                destination: .settings(.importExportSettings)\n            )\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"General\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/HapticSettingsView.swift",
    "content": "//\n//  HapticSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-31.\n//\n\nimport Haptics\nimport SwiftUI\nimport Theming\n\nstruct HapticSettingsView: View {\n    @Setting(\\.behavior_hapticLevel) var hapticLevel\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Haptics\",\n                description: \"Customize how often Mlem plays haptic feedback.\",\n                icon: .general.haptics\n            )\n            .gradientTint(.themedColorfulAccent(1))\n            Picker(\"Haptic Level\", selection: $hapticLevel) {\n                ForEach(HapticTier.allCases, id: \\.self) { level in\n                    Text(level.label)\n                        .tag(level as HapticTier?)\n                }\n                Text(\"None\").tag(nil as HapticTier?)\n            }\n            .pickerStyle(.inline)\n            .labelsHidden()\n        }\n        .contentMargins(.top, 16)\n        .withConditionalLabelStyle()\n        .hiddenNavigationTitle(\"Haptics\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Icon/AlternateIcon.swift",
    "content": "//\n//  AlternateIcon.swift\n//  Mlem\n//\n//  Created by tht7 on 28/06/2023.\n//\n\nimport Foundation\n\nstruct AlternateIcon: Identifiable {\n    var id: String?\n    let name: String\n    \n    init(id: String?, name: LocalizedStringResource) {\n        self.id = id\n        self.name = String(localized: name)\n    }\n    \n    @_disfavoredOverload\n    init(id: String?, name: String) {\n        self.id = id\n        self.name = name\n    }\n}\n\nstruct AlternateIconGroup {\n    let authorName: String\n    let collapsed: Bool\n    let icons: [AlternateIcon]\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Icon/AlternateIconCell.swift",
    "content": "//\n//  AlternateIconCell.swift\n//  Mlem\n//\n//  Created by tht7 on 28/06/2023.\n//\n\nimport SwiftUI\n\nstruct AlternateIconCell: View {\n    let icon: AlternateIcon\n    let setAppIcon: (_ id: String?) async -> Void\n    let selected: Bool\n\n    var body: some View {\n        Button {\n            Task(priority: .userInitiated) {\n                await setAppIcon(icon.id)\n            }\n        } label: {\n            AlternateIconLabel(icon: icon, selected: selected)\n        }.accessibilityElement(children: .combine)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Icon/AlternateIconLabel.swift",
    "content": "//\n//  AlternateIconCell.swift\n//  Mlem\n//\n//  Created by tht7 on 28/06/2023.\n//\n\nimport SwiftUI\n\nstruct AlternateIconLabel: View {\n    let icon: AlternateIcon\n    let selected: Bool\n\n    var body: some View {\n        VStack {\n            getImage()\n                .resizable()\n                .scaledToFit()\n                .frame(width: Constants.main.appIconSize, height: Constants.main.appIconSize)\n                .cornerRadius(Constants.main.appIconCornerRadius)\n                .padding(3)\n                .shadow(radius: 2, x: 0, y: 2)\n                .overlay {\n                    if selected {\n                        ZStack {\n                            RoundedRectangle(cornerRadius: Constants.main.appIconCornerRadius)\n                                .stroke(.themedSecondaryGroupedBackground, lineWidth: 5)\n                                .padding(2)\n                            RoundedRectangle(cornerRadius: Constants.main.appIconCornerRadius + 2)\n                                .stroke(.themedAccent, lineWidth: 3)\n                        }\n                    }\n                }\n            Text(icon.name)\n                .multilineTextAlignment(.center)\n                .font(.footnote)\n                .foregroundStyle(selected ? .themedAccent : .themedSecondary)\n        }\n    }\n    \n    func getImage() -> Image {\n        let iconId: String\n        if let id = icon.id {\n            iconId = \"\\(id).preview\"\n        } else {\n            iconId = \"logo\"\n        }\n        return .init(uiImage: .init(named: iconId) ?? .init())\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/Icon/IconSettingsView.swift",
    "content": "//\n//  Alternate Icons.swift\n//  Mlem\n//\n//  Created by tht7 on 28/06/2023.\n//\n\nimport SwiftUI\nimport Theming\n\n// struct AlternateIcons: View {\nstruct IconSettingsView: View {\n    @State var currentIcon: String? = UIApplication.shared.alternateIconName\n    \n    let icons: [AlternateIconGroup] = [\n        .init(authorName: \"Sjmarf\", collapsed: false, icons: [\n            .init(id: nil, name: \"Default\"),\n            .init(id: \"icon.sjmarf.pink\", name: \"Pink\"),\n            .init(id: \"icon.sjmarf.orange\", name: \"Orange\"),\n            .init(id: \"icon.sjmarf.green\", name: \"Green\"),\n            .init(id: \"icon.sjmarf.alien\", name: \"Alien\"),\n            .init(id: \"icon.sjmarf.silver\", name: \"Silver\"),\n            .init(id: \"icon.sjmarf.ocean\", name: \"Ocean\"),\n            .init(id: \"icon.sjmarf.pride\", name: \"Pride\")\n        ]),\n//\n        .init(authorName: \"Eric Andrews\", collapsed: false, icons: [\n            .init(id: \"icon.eric.lemmy\", name: \"Lemmy\")\n        ])\n    ]\n\n    var body: some View {\n        FancyScrollView {\n            VStack(spacing: 32) {\n                ForEach(icons, id: \\.authorName) { group in\n                    if !group.icons.isEmpty {\n                        CollapsibleSection(group.authorName, collapsed: group.collapsed) {\n                            LazyVGrid(columns: .init(repeating: GridItem(.flexible()), count: 4), spacing: 10, content: {\n                                ForEach(group.icons) { icon in\n                                    AlternateIconCell(\n                                        icon: icon,\n                                        setAppIcon: setAppIcon,\n                                        selected: currentIcon == icon.id\n                                    )\n                                }\n                            })\n                            .padding(.vertical, 15)\n                            .padding(.horizontal, 12)\n                        }\n                    }\n                }\n            }\n            .padding(.vertical)\n        }\n        .themedGroupedBackground()\n        .navigationTitle(\"App Icon\")\n    }\n\n    @MainActor\n    func setAppIcon(_ id: String?) async {\n        do {\n            try await UIApplication.shared.setAlternateIconName(id)\n            currentIcon = id\n        } catch {\n            // do nothing!\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ImageViewerDismissSettingsView.swift",
    "content": "//\n//  ImageViewerDismissSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-20.\n//\n\nimport SwiftUI\n\nstruct ImageViewerDismissSettingsView: View {\n    @Setting(\\.imageViewer_dismissThreshold) var dismissThreshold\n\n    @State var sliderValue: Double\n\n    init() {\n        let threshold = Settings.get(\\.imageViewer_dismissThreshold)\n        self._sliderValue = .init(initialValue: 21 - Double(threshold))\n    }\n\n    var body: some View {\n        Form {\n            headerView\n            sliderView\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Dismiss Sensitivity\")\n    }\n\n    @ViewBuilder\n    var headerView: some View {\n        SettingsHeaderView(\n            title: \"Dismiss Sensitivity\",\n            description: \"Choose how far you have to drag to dismiss the image viewer.\",\n            icon: .settings.imageViewerDismissSensitivity\n        )\n        .gradientTint(.themedColorfulAccent(5))\n    }\n\n    @ViewBuilder\n    var sliderView: some View {\n        Section {\n            VStack(spacing: 5) {\n                HStack {\n                    Text(\"Low\")\n                    Spacer()\n                    Text(\"High\")\n                }\n                .font(.footnote)\n                .foregroundStyle(.themedSecondary)\n\n                // I tried using Binding(get: set:) here, but it caused haptics to be\n                // spammed if you move the handle to either end of the slider.\n                Slider(\n                    value: $sliderValue,\n                    in: 1...20\n                ) { pressed in\n                    if !pressed {\n                        self.dismissThreshold = 21 - Int(sliderValue.rounded())\n                    }\n                }\n            }\n        } footer: {\n            Button {\n                self.dismissThreshold = 10\n                sliderValue = 21 - 10\n            } label: {\n                // `Label` has too wide spacing\n                HStack(spacing: 5) {\n                    Image(icon: .general.refresh)\n                    Text(\"Reset\")\n                }\n            }\n            .font(.footnote)\n            .labelStyle(.titleAndIcon)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ImageViewerSettingsView.swift",
    "content": "//\n//  ImageViewerSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-20.\n//\n\nimport SwiftUI\n\nstruct ImageViewerSettingsView: View {\n    @Setting(\\.a11y_zoomSliderLocation) var zoomSliderLocation\n    @Setting(\\.imageViewer_showControls) var showControls\n    @Setting(\\.imageViewer_showCloseButton) var showCloseButton\n    @Setting(\\.imageViewer_showZoomIndicator) var showZoomIndicator\n    @Setting(\\.imageViewer_dismissThreshold) var dismissThreshold\n\n    var body: some View {\n        Form {\n            headerView\n            controlsSectionView\n            gesturesSectionView\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Image Viewer\")\n    }\n\n    @ViewBuilder\n    var headerView: some View {\n        SettingsHeaderView(\n            title: \"Image Viewer\",\n            description: \"Customize the image viewer's buttons and gestures.\",\n            icon: .settings.imageViewer\n        )\n        .gradientTint(.themedColorfulAccent(5))\n    }\n\n    @ViewBuilder\n    var controlsSectionView: some View {\n        Section(\"Controls\") {\n            NavigationLink(\n                \"Show Controls\",\n                value: .init(localized: showControls.label),\n                fallbackValue: \"\",\n                icon: .general.circle,\n                destination: .settings(.imageViewerControls)\n            )\n            Toggle(\"Close Button\", icon: .general.close, isOn: $showCloseButton)\n            Toggle(\"Zoom Indicator\", icon: .general.search, isOn: $showZoomIndicator)\n        }\n    }\n\n    @ViewBuilder\n    var gesturesSectionView: some View {\n        Section(\"Gestures\") {\n            NavigationLink(\n                \"Dismiss Sensitivity\",\n                value: .init(localized: dismissSensitivityLabel),\n                fallbackValue: \"\",\n                icon: .settings.imageViewerDismissSensitivity,\n                destination: .settings(.imageViewerDismissSensitivity)\n            )\n            NavigationLink(\n                \"Slide to Zoom\",\n                value: .init(localized: zoomSliderLocation.label),\n                fallbackValue: \"\",\n                icon: .settings.zoomSlider,\n                destination: .settings(.zoomSlider)\n            )\n        }\n    }\n\n    var dismissSensitivityLabel: LocalizedStringResource {\n        switch dismissThreshold {\n        case 1: \"Highest\"\n        case 2...6: \"High\"\n        case 10: \"Default\"\n        case 15...19: \"Low\"\n        case 20: \"Lowest\"\n        default: \"Medium\"\n        }\n    }\n}\n\nenum ShowImageViewerControls: String, Codable, CaseIterable {\n    case immediately, onTap, never\n\n    var label: LocalizedStringResource {\n        switch self {\n        case .immediately: \"Immediately\"\n        case .onTap: \"When I Tap\"\n        case .never: \"Never\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ImageViewerShowControlsSettingsView.swift",
    "content": "//\n//  ImageViewerShowControlsSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-20.\n//\n\nimport SwiftUI\n\nstruct ImageViewerShowControlsSettingsView: View {\n    @Setting(\\.imageViewer_showControls) var showControls\n\n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Show Controls\",\n                description: \"Choose when the image viewer controls should appear.\",\n                icon: .settings.imageViewerControls\n            )\n            .gradientTint(.themedColorfulAccent(5))\n            Picker(\"Show Controls\", selection: $showControls) {\n                ForEach(ShowImageViewerControls.allCases, id: \\.self) { value in\n                    Text(value.label)\n                }\n            }\n            .pickerStyle(.inline)\n            .labelsHidden()\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Show Controls\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ImportExportSettingsView.swift",
    "content": "//\n//  ImportExportSettingsPage.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-09-06.\n//\n\nimport Dependencies\nimport Foundation\nimport SwiftUI\n\n// DO NOT use Picker in this page! It refuses to change the theme until it gets re-rendered. All other components pick up theme changes correctly.\nstruct ImportExportSettingsView: View {\n    @Dependency(\\.persistenceRepository) var persistenceRepository\n    \n    @Environment(NavigationLayer.self) var navigation\n    \n    @State var importingSettingsFile: Bool = false\n    \n    // these are tracked as state vars so they can be updated as appropriate\n    @State var v1SettingsExist: Bool = false\n    @State var v2SettingsExist: Bool = false\n    \n    var body: some View {\n        content\n            .withConditionalLabelStyle()\n            .onAppear {\n                v1SettingsExist = persistenceRepository.systemSettingsExists(.v1_user)\n                v2SettingsExist = persistenceRepository.systemSettingsExists(.v2_user)\n            }\n            .fileImporter(\n                isPresented: $importingSettingsFile,\n                allowedContentTypes: [.json]\n            ) { result in\n                do {\n                    let fileUrl = try result.get()\n                    if let fileData = readSettings(from: fileUrl) {\n                        let importedSettings = try JSONDecoder().decode(SettingsValues.self, from: fileData)\n                        Settings.reinit(with: importedSettings)\n                        ToastModel.main.add(.success(\"Imported Settings\"))\n                    } else {\n                        assertionFailure(\"Failed to import settings\")\n                        ToastModel.main.add(.failure(\"Failed to import settings\"))\n                    }\n                } catch {\n                    handleError(error)\n                }\n            }\n    }\n    \n    var content: some View {\n        Form {\n            Section(\"Save and Restore\") {\n                Button(\"Save Settings\", icon: .settings.saveSettings) {\n                    Task {\n                        await Settings.save(to: .v2_user)\n                        v2SettingsExist = persistenceRepository.systemSettingsExists(.v2_user)\n                    }\n                }\n                \n                Button(\"Restore Settings\", icon: .settings.restoreSettings) {\n                    Task { @MainActor in\n                        Settings.restore(from: .v2_user)\n                    }\n                }\n                .disabled(!v2SettingsExist)\n            } footer: {\n                Text(\"Save the current settings and restore them later.\")\n            }\n            \n            Section {\n                Button(\"Export Settings\", icon: .general.export) {\n                    Task {\n                        let data = try Settings.encoded()\n                        let fileUrl = FileManager.default.temporaryDirectory.appending(path: \"settings.json\")\n                        try data.write(to: fileUrl, options: .atomic)\n                        navigation.model?.shareInfo = .init(url: fileUrl)\n                    }\n                }\n                \n                Button(\"Import Settings\", icon: .general.import) {\n                    importingSettingsFile = true\n                }\n            }\n        }\n    }\n    \n    func readSettings(from fileUrl: URL) -> Data? {\n        let accessing = fileUrl.startAccessingSecurityScopedResource()\n        \n        // ensure we relinquish access\n        defer {\n            if accessing {\n                fileUrl.stopAccessingSecurityScopedResource()\n            }\n        }\n        \n        do {\n            return try Data(contentsOf: fileUrl, options: .mappedIfSafe)\n        } catch {\n            handleError(error)\n            return nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/InboxBadgeSettingsView.swift",
    "content": "//\n//  InboxBadgeSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-17.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct InboxBadgeSettingsView: View {\n    @Setting(\\.tab_inbox_badgeIncludedTypes) var tabInboxBadgeIncludedTypes\n    \n    var body: some View {\n        Form {\n            headerView\n            Section {\n                toggle(forType: .reply)\n                toggle(forType: .mention)\n                toggle(forType: .message)\n            }\n            if AccountsTracker.main.highestLevelAccountType >= .moderator {\n                Section {\n                    toggle(forType: .postReport)\n                    toggle(forType: .commentReport)\n                    if AccountsTracker.main.highestLevelAccountType == .admin {\n                        toggle(forType: .messageReport)\n                        toggle(forType: .registrationApplication)\n                    }\n                }\n            }\n        }\n        .contentMargins(.top, 16, for: .scrollContent)\n        .withConditionalLabelStyle()\n        .hiddenNavigationTitle(\"Notification Badge\")\n    }\n    \n    @ViewBuilder\n    var headerView: some View {\n        SettingsHeaderView(\n            title: \"Notification Badge\",\n            description: \"Configure which types of notification should be included in the notification badge.\"\n        ) {\n            Image(icon: .lemmy.inbox)\n                .resizable()\n                .symbolVariant(.fill)\n                .aspectRatio(contentMode: .fit)\n                .frame(width: 64)\n                .foregroundStyle(.tertiary)\n                .padding([.trailing, .top], 20)\n                .overlay(alignment: .topTrailing) {\n                    Text(verbatim: \"1\")\n                        .font(.title2)\n                        .foregroundStyle(.themedContrastingLabel)\n                        .aspectRatio(1, contentMode: .fit)\n                        .padding(10)\n                        .background(.themedWarning, in: .circle)\n                }\n                .padding(.top, -10)\n        }\n    }\n    \n    @ViewBuilder\n    func toggle(forType type: InboxItemType) -> some View {\n        Toggle(type.label, icon: type.icon, isOn: .init(\n            get: { tabInboxBadgeIncludedTypes.contains(type) },\n            set: {\n                if $0 {\n                    tabInboxBadgeIncludedTypes.insert(type)\n                } else {\n                    tabInboxBadgeIncludedTypes.remove(type)\n                }\n            }\n        ))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/InboxSettingsView.swift",
    "content": "//\n//  InboxSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct InboxSettingsView: View {\n    @Setting(\\.tab_inbox_badgeIncludedTypes) var tabInboxBadgeIncludedTypes\n    @Setting(\\.interactionBar_reply) var replyInteractionBar\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Inbox\",\n                // swiftlint:disable:next line_length\n                description: \"Customize the interaction bar for inbox items, and choose which types of notification are included in the tab bar badge.\",\n                icon: .lemmy.inbox\n            )\n            .gradientTint(.themedInbox)\n            Section {\n                NavigationLink(.settings(.interactionBar(.inboxNotification))) {\n                    SettingsInteractionBarSummaryView(configuration: replyInteractionBar)\n                }\n                NavigationLink(\"Swipe Actions\", destination: .settings(.swipeActions(.inboxNotification)))\n            }\n            if AccountsTracker.main.highestLevelAccountType >= .moderator {\n                Section {\n                    NavigationLink(\n                        \"Mod Mail Action Layouts\",\n                        icon: .settings.interactionBar,\n                        destination: .settings(.modMailInteractionBar)\n                    )\n                }\n            }\n            Section {\n                NavigationLink(\n                    \"Notification Badge\",\n                    value: tabInboxBadgeIncludedTypes.label(accountType: AccountsTracker.main.highestLevelAccountType),\n                    fallbackValue: .init(localized: \"Some\"),\n                    icon: .settings.unreadBadge,\n                    destination: .settings(.inboxBadge)\n                )\n            }\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Inbox\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView+Logic.swift",
    "content": "//\n//  InteractionBarEditorView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 18/08/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nextension InteractionBarEditorView {\n    // MARK: - Definitions\n    \n    enum ConfigurationType {\n        case post, comment\n    }\n    \n    enum DropLocation: Equatable {\n        case bar(Int)\n        case tray\n        \n        var index: Int? {\n            switch self {\n            case let .bar(index): index\n            default: nil\n            }\n        }\n    }\n    \n    @Observable\n    class TrayItem: Equatable {\n        let item: Configuration.Item\n        \n        private(set) var opacity: CGFloat\n        \n        init(item: Configuration.Item, visible: Bool) {\n            self.item = item\n            self.opacity = visible ? 1 : 0\n        }\n        \n        /// Toggles opacity to 1\n        func show() { opacity = 1 }\n        \n        /// Toggles opacity to 0\n        func hide() { opacity = 0 }\n        \n        static func == (lhs: TrayItem, rhs: TrayItem) -> Bool {\n            lhs.item == rhs.item\n        }\n    }\n     \n    @Observable\n    class BarItem: Equatable {\n        let item: Configuration.Item?\n        \n        /// Controls the width of the barItem view\n        private(set) var maxWidth: CGFloat?\n        \n        /// Controls the opacity of the barItem view\n        private(set) var opacity: CGFloat\n        \n        /// If this BarItem is replacing another one (i.e., when moving a widget on the bar), this points to the old\n        /// BarItem, allowing the barItem view to smoothly animate the ancestor out when it appears.\n        weak var ancestor: BarItem?\n        \n        /// Uniquely identifies this BarItem. This is needed to allow two `BarItem`s with the same `item`\n        /// to exist at once on the bar (used when moving bar items) without relying on index-based identification\n        let uuid: UUID = .init()\n        \n        init(item: Configuration.Item?, expanded: Bool, visible: Bool, ancestor: BarItem? = nil) {\n            self.item = item\n            self.maxWidth = expanded ? nil : 0\n            self.opacity = visible ? 1 : 0\n            self.ancestor = ancestor\n        }\n        \n        /// Expands maxWidth to default\n        func expand() { maxWidth = nil }\n        \n        /// Reduces maxWidth to 0\n        func collapse() { maxWidth = 0 }\n        \n        /// Toggles opacity to 1\n        func show() { opacity = 1 }\n        \n        /// Toggles opacity to 0\n        func hide() { opacity = 0 }\n        \n        static func == (lhs: BarItem, rhs: BarItem) -> Bool {\n            lhs.uuid == rhs.uuid\n        }\n    }\n    \n    // MARK: - Helper Computed Vars\n    \n    var allowNewItemInsertion: Bool {\n        if let trayPickedUpItem {\n            let currentScore = barItems.reduce(0) { $0 + ($1.item?.score ?? 0) }\n            return currentScore + trayPickedUpItem.item.score <= 6\n        }\n        return true\n    }\n    \n    var showInfoCapsule: Bool { !allowNewItemInsertion || trayPickedUpItem != nil }\n    \n    var isDraggingItem: Bool { trayPickedUpItem != nil || barPickedUpItem != nil }\n    \n    var barPickedUpIndex: Int? { barPickedUpItem?.index }\n    \n    // MARK: - Drag Gestures\n    \n    func barItemDragGesture(item: BarItem, index: Int) -> some Gesture {\n        DragGesture(minimumDistance: 0, coordinateSpace: .named(\"editor\"))\n            .onChanged { gesture in\n                if barPickedUpItem == nil {\n                    hapticManager.play(haptic: .firmInfo, tier: .low)\n                    barPickedUpItem = (item, index)\n                    if let trayItem = trayItems.first(where: { $0.item == item.item }) {\n                        withAnimation(.easeOut(duration: barAnimationDuration)) {\n                            trayItem.hide()\n                        }\n                    }\n                }\n                dragLocation = gesture.location\n                dragTranslation = gesture.translation\n            }\n            .onEnded { _ in\n                completeDrag()\n            }\n    }\n    \n    func trayItemDragGesture(trayItem: TrayItem) -> some Gesture {\n        DragGesture(minimumDistance: 0, coordinateSpace: .named(\"editor\"))\n            .onChanged { gesture in\n                if trayPickedUpItem == nil {\n                    hapticManager.play(haptic: .firmInfo, tier: .low)\n                    trayPickedUpItem = trayItem\n                }\n                dragLocation = gesture.location\n                dragTranslation = gesture.translation\n            }\n            .onEnded { _ in\n                completeDrag()\n            }\n    }\n    \n    func completeDrag() {\n        defer {\n            self.barPickedUpItem = nil\n            self.dropLocation = nil\n            self.trayPickedUpItem = nil\n        }\n        \n        guard let dropLocation else { return }\n  \n        if let trayPickedUpItem {\n            guard case let .bar(targetIndex) = dropLocation else { return }\n            addToBar(trayPickedUpItem, at: targetIndex)\n        } else if let barPickedUpItem {\n            if let trayItem = trayItems.first(where: { $0.item == barPickedUpItem.barItem.item }) {\n                withAnimation(.easeOut(duration: barAnimationDuration)) {\n                    trayItem.show()\n                }\n            }\n            \n            switch dropLocation {\n            case let .bar(targetIndex):\n                moveOnBar(barItem: barPickedUpItem.barItem, from: barPickedUpItem.index, to: targetIndex)\n            case .tray:\n                removeFromBar(barItem: barPickedUpItem.barItem, barItemIndex: barPickedUpItem.index)\n            }\n        }\n    }\n    \n    // MARK: - State Updates\n    \n    func addToBar(_ trayItem: TrayItem, at index: Int) {\n        guard allowNewItemInsertion else {\n            assertionFailure(\"Item insertion disabled\")\n            return\n        }\n        \n        hapticManager.play(haptic: .firmInfo, tier: .high)\n\n        let newItem: BarItem = .init(item: trayItem.item, expanded: false, visible: true)\n        \n        barItems.insert(newItem, at: index)\n        \n        // gently fade the tray item back in\n        trayItem.hide()\n        withAnimation(.easeOut(duration: trayItemDuration)) {\n            trayItem.show()\n        }\n        \n        // recompute infoStackAlignment with actual barItems, since these animations all play nice\n        withAnimation(.easeInOut(duration: barAnimationDuration)) {\n            infoStackAlignment = computeInfoStackAlignment(\n                infoStackIndex: infoStackIndex(),\n                totalItems: barItems.count\n            )\n        }\n        \n        updateConfiguration()\n    }\n  \n    func moveOnBar(barItem: BarItem, from sourceIndex: Int, to targetIndex: Int) {\n        // noop on move to current location or immediately after current location\n        guard targetIndex != sourceIndex, targetIndex != sourceIndex + 1 else { return }\n        \n        hapticManager.play(haptic: .firmInfo, tier: .high)\n        \n        let newItem: BarItem = .init(item: barItem.item, expanded: false, visible: true, ancestor: barItem)\n        barItem.hide()\n        \n        if targetIndex == barItems.count {\n            barItems.append(newItem)\n        } else {\n            barItems.insert(newItem, at: targetIndex)\n        }\n        \n        // recompute infoStackAlignment with projected info stack location\n        let infoStackIndex = infoStackIndex()\n        let newInfoStackAlignment: Alignment?\n        if barItem.item == nil {\n            // if moving info stack itself, can compute alignment based on whether moving to beginning or end\n            if targetIndex == 0 {\n                newInfoStackAlignment = barItems.count == 1 ? .center : .leading\n            } else if targetIndex == barItems.count - 1 {\n                newInfoStackAlignment = .trailing\n            } else {\n                newInfoStackAlignment = .center\n            }\n        } else {\n            if sourceIndex < infoStackIndex {\n                if targetIndex > infoStackIndex {\n                    // moving widget from left to right of info stack, projected infostack index is current - 1\n                    newInfoStackAlignment = computeInfoStackAlignment(\n                        infoStackIndex: infoStackIndex - 1,\n                        totalItems: barItems.count\n                    )\n                } else {\n                    // widget not moving \"over\" the info stack, no change\n                    newInfoStackAlignment = nil\n                }\n            } else {\n                if targetIndex < infoStackIndex {\n                    // moving widget from right to left of info stack, projected infostack index is current + 1\n                    newInfoStackAlignment = computeInfoStackAlignment(\n                        infoStackIndex: infoStackIndex + 1,\n                        totalItems: barItems.count\n                    )\n                } else {\n                    // widget not moving \"over\" the info stack, no change\n                    newInfoStackAlignment = nil\n                }\n            }\n        }\n        if let newInfoStackAlignment {\n            withAnimation(.easeInOut(duration: barAnimationDuration)) { infoStackAlignment = newInfoStackAlignment }\n        }\n        \n        // wait for animation to complete, then remove original item from barItems\n        DispatchQueue.main.asyncAfter(deadline: .now() + barAnimationDuration) {\n            barItems.removeAll(where: { $0 == barItem })\n            updateConfiguration()\n        }\n    }\n    \n    func removeFromBar(barItem: BarItem, barItemIndex: Int) {\n        // no removing the info stack\n        guard barItem.item != nil else { return }\n        \n        hapticManager.play(haptic: .firmInfo, tier: .high)\n        \n        // recompute infoStackAlignment with projected info stack location\n        let infoStackIndex = infoStackIndex()\n        let newInfoStackAlignment: Alignment\n        if barItemIndex < infoStackIndex {\n            // removing item to the left of info stack: shift info stack left\n            newInfoStackAlignment = computeInfoStackAlignment(infoStackIndex: infoStackIndex - 1, totalItems: barItems.count - 1)\n        } else {\n            // removing item to the right of info stack: info stack index unchanged, but barItems.count still decreases\n            newInfoStackAlignment = computeInfoStackAlignment(infoStackIndex: infoStackIndex, totalItems: barItems.count - 1)\n        }\n\n        // smoothly animate away\n        barItem.hide()\n        withAnimation(.easeInOut(duration: barAnimationDuration)) {\n            barItem.collapse()\n            if newInfoStackAlignment != infoStackAlignment {\n                infoStackAlignment = newInfoStackAlignment\n            }\n        }\n        \n        // wait for animation to complete, then remove from barItems\n        DispatchQueue.main.asyncAfter(deadline: .now() + barAnimationDuration) {\n            barItems.removeAll(where: { $0 == barItem })\n            updateConfiguration()\n        }\n    }\n    \n    func updateConfiguration() {\n        guard let infoStackIndex = barItems.firstIndex(where: { $0.item == nil }) else {\n            assertionFailure(\"Could not find info stack in barItems\")\n            return\n        }\n        configuration = .init(\n            leading: barItems[..<infoStackIndex].compactMap(\\.item),\n            trailing: barItems[infoStackIndex...].compactMap(\\.item),\n            savedSwipes: configuration.savedSwipes,\n            readouts: configuration.readouts,\n            availableWidgets: configuration.availableWidgets,\n            savedContextMenu: configuration.savedContextMenu\n        )\n    }\n    \n    // MARK: - Helpers\n    \n    func trayItemOutlineColor(_ trayItem: TrayItem) -> ThemedColor {\n        if let dropLocation,\n           trayPickedUpItem == trayItem || (barPickedUpItem?.barItem.item == trayItem.item && dropLocation == .tray) {\n            return .themedAccent\n        }\n        return .themedTertiary\n    }\n    \n    func infoStackIndex() -> Int {\n        guard let ret = barItems.firstIndex(where: { $0.item == nil }) else {\n            assertionFailure(\"could not find infoStack index\")\n            return 0\n        }\n        return ret\n    }\n}\n\nfunc computeInfoStackAlignment(infoStackIndex: Int, totalItems: Int) -> Alignment {\n    if infoStackIndex == 0 {\n        return totalItems == 1 ? .center : .leading\n    } else if infoStackIndex == totalItems - 1 {\n        return .trailing\n    } else {\n        return .center\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView+Views.swift",
    "content": "//\n//  InteractionBarEditorView+Views.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-01-27.\n//\n\nimport ComponentViews\nimport Flow\nimport SwiftUI\nimport Theming\n\n// swiftlint:disable file_length\n\nextension InteractionBarEditorView {\n    // MARK: - Previews\n    \n    @ViewBuilder\n    var contentPreview: some View {\n        VStack(alignment: .leading, spacing: 0) {\n            Group {\n                switch configurationType {\n                case .post: postPreviewBody\n                case .comment: commentPreviewBody\n                }\n            }\n            .opacity(0.75)\n            .padding([.top, .horizontal], Constants.main.standardSpacing)\n            \n            interactionBar\n                .frame(height: Constants.main.barIconHitbox)\n        }\n        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.mediumItemCornerRadius))\n        .paletteBorder(cornerRadius: Constants.main.mediumItemCornerRadius)\n    }\n    \n    @ViewBuilder\n    var postPreviewBody: some View {\n        HStack(alignment: .top, spacing: 8) {\n            RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius)\n                .fill(.themedAccent.opacity(0.6))\n                .frame(width: Constants.main.thumbnailSize, height: Constants.main.thumbnailSize)\n                .overlay {\n                    Image(systemName: \"mountain.2.fill\")\n                        .font(.system(size: 23))\n                        .foregroundStyle(.white)\n                }\n            \n            VStack(alignment: .leading, spacing: 5) {\n                MockTextView()\n                    .frame(maxWidth: .infinity)\n                    .frame(height: 15)\n                MockTextView()\n                    .frame(maxWidth: 200)\n                    .frame(height: 15)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var commentPreviewBody: some View {\n        HStack(spacing: 7) {\n            Image(icon: .lemmy.personAvatar)\n                .resizable()\n                .scaledToFit()\n                .symbolVariant(.circle.fill)\n                .symbolRenderingMode(.palette)\n                .foregroundStyle(palette.contrastingLabel, palette.neutralAccent.gradient)\n                .frame(width: Constants.main.smallAvatarSize, height: Constants.main.smallAvatarSize)\n                .compositingGroup()\n                .opacity(0.5)\n            \n            MockTextView(beginOpacity: 0.4, endOpacity: 0.3)\n                .frame(maxWidth: 200)\n                .frame(height: 13)\n        }\n        \n        VStack(alignment: .leading, spacing: 5) {\n            MockTextView()\n                .frame(maxWidth: .infinity)\n                .frame(height: 15)\n            MockTextView()\n                .frame(maxWidth: 250)\n                .frame(height: 15)\n        }\n    }\n    \n    @ViewBuilder\n    var interactionBar: some View {\n        HStack(spacing: 0) {\n            ForEach(Array(barItems.enumerated()), id: \\.element.uuid) { index, item in\n                if dropLocation?.index == index,\n                   barPickedUpIndex != index,\n                   barPickedUpIndex != index - 1 {\n                    dropIndicator(index: index)\n                }\n                \n                barItem(item, index: index)\n            }\n            \n            if dropLocation?.index == barItems.count,\n               barPickedUpIndex != barItems.count - 1 {\n                dropIndicator(index: barItems.count)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func barItem(_ barItem: BarItem, index: Int) -> some View {\n        itemLabel(barItem.item)\n            .offset(barPickedUpIndex == index ? dragTranslation : .zero)\n            .background {\n                if barPickedUpIndex == index, dragTranslation != .zero {\n                    Capsule()\n                        .fill(.themedAccent.opacity(0.2))\n                        .stroke(.themedAccent)\n                        .padding(4)\n                }\n            }\n            .overlay {\n                GeometryReader { geometry in\n                    Color.clear\n                        .contentShape(.rect)\n                        .frame(maxWidth: .infinity, maxHeight: .infinity)\n                        .onChange(of: dragLocation) {\n                            guard allowNewItemInsertion, isDraggingItem else { return }\n                            \n                            let frame = geometry.frame(in: .named(\"editor\"))\n                            \n                            // if outside of bar zone, reset newHoveredDropLocation\n                            guard dragLocation.y <= frame.maxY + 30 else {\n                                dropLocation = .tray\n                                return\n                            }\n                            \n                            // check if within this item's hitbox\n                            if dragLocation.x > frame.minX,\n                               dragLocation.x < frame.maxX {\n                                // determine whether hovered over the left or the right side, update hoveredDropIndex accordingly\n                                dropLocation = .bar(dragLocation.x < frame.midX ? index : index + 1)\n                            }\n                        }\n                }\n            }\n            .gesture(barItemDragGesture(item: barItem, index: index))\n            .onAppear {\n                withAnimation(.easeOut(duration: barAnimationDuration)) {\n                    barItem.ancestor?.collapse()\n                    barItem.expand()\n                }\n            }\n            .frame(maxWidth: barItem.maxWidth)\n            .opacity(barItem.opacity)\n            .zIndex(barPickedUpIndex == index ? 2 : 0)\n    }\n    \n    // MARK: - Palette\n    \n    @ViewBuilder\n    var tray: some View {\n        HFlow(horizontalAlignment: .center, verticalAlignment: .center, distributeItemsEvenly: true) {\n            ForEach(trayItems, id: \\.item) { trayItem($0) }\n        }\n    }\n    \n    @ViewBuilder\n    func trayItem(_ trayItem: TrayItem) -> some View {\n        itemLabel(trayItem.item)\n            .opacity(trayItem.opacity)\n            .geometryGroup()\n            .offset(trayPickedUpItem == trayItem ? dragTranslation : .zero)\n            .background {\n                Group {\n                    switch trayItem.item {\n                    case let .action(action):\n                        InteractionBarActionLabelView(action.appearance)\n                    case let .counter(counter):\n                        counterLabel(counter.appearance)\n                            .fixedSize()\n                    }\n                }\n                .opacity(0.2)\n                .background {\n                    Capsule()\n                        .fill(trayItemOutlineColor(trayItem).opacity(0.2))\n                        .stroke(trayItemOutlineColor(trayItem))\n                        .background(.themedSecondaryGroupedBackground, in: .capsule)\n                }\n            }\n            .gesture(trayItemDragGesture(trayItem: trayItem))\n            .zIndex(trayPickedUpItem == trayItem ? 2 : 0)\n    }\n    \n    @ViewBuilder\n    var readoutSelectors: some View {\n        HFlow(spacing: Constants.main.standardSpacing) {\n            ForEach(Array(Configuration.ReadoutType.allCases.enumerated()), id: \\.offset) { _, readout in\n                let isActive = configuration.readouts.contains(readout)\n                let disabled = !readout.compatibleWith(otherReadouts: Set(configuration.readouts))\n                Button {\n                    if isActive {\n                        if let index = configuration.readouts.firstIndex(of: readout) {\n                            configuration.readouts.remove(at: index)\n                        }\n                    } else {\n                        // Insert and sort the new `ReadoutType`. In future these could be re-arrangable too\n                        // but I need to think about how the UI would work\n                        configuration.readouts = Configuration.ReadoutType.allCases.filter {\n                            configuration.readouts.contains($0) || $0 == readout\n                        }\n                    }\n                    hapticManager.play(haptic: .gentleInfo, tier: .low)\n                } label: {\n                    let color: ThemedColor = disabled ? .themedPrimary : .themedAccent\n                    HStack(spacing: 2) {\n                        Image(icon: readout.appearance.icon.representingState(active: false))\n                        if readout.appearance.label != \"\" {\n                            Text(readout.appearance.label)\n                        }\n                    }\n                    .font(.footnote)\n                    .foregroundStyle(isActive ? .themedContrastingLabel : color)\n                    .padding(.horizontal, 12)\n                    .padding(.vertical, 8)\n                    .background {\n                        Capsule().fill(isActive ? color : color.opacity(0.2)).stroke(color)\n                    }\n                    .transaction { $0.animation = nil }\n                }\n                .buttonStyle(.plain)\n                .disabled(disabled)\n            }\n        }\n    }\n    \n    // MARK: - General Page Views\n    \n    @ViewBuilder\n    var header: some View {\n        SettingsHeaderView(\n            title: \"Interaction Bar\",\n            description: \"Tap and hold items to add, remove, or rearrange them.\"\n        ) {}\n            .padding(Constants.main.standardSpacing)\n            .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.largeItemCornerRadius))\n    }\n    \n    @ViewBuilder\n    var infoCapsule: some View {\n        if !allowNewItemInsertion {\n            Text(\"Too many items\")\n                .padding(7.5)\n                .padding(.horizontal, 5)\n                .foregroundStyle(.themedNegative)\n                .background {\n                    Capsule()\n                        .fill(.themedNegative.opacity(0.2))\n                        .stroke(.themedNegative)\n                        .background(.themedSecondaryGroupedBackground, in: .capsule)\n                }\n                .frame(height: infoCapsuleHeight)\n        } else if let trayPickedUpItem {\n            Group {\n                switch trayPickedUpItem.item {\n                case let .action(action):\n                    HStack {\n                        Image(systemName: action.appearance.barIcon)\n                        Text(action.appearance.label)\n                    }\n                case let .counter(counter):\n                    HStack {\n                        counterLabel(counter.appearance)\n                            .fixedSize()\n                        Text(counter.appearance.label)\n                    }\n                }\n            }\n            .padding(7.5)\n            .padding(.horizontal, 5)\n            .background {\n                Capsule()\n                    .fill(.themedSecondaryGroupedBackground)\n                    .stroke(.themedTertiary)\n            }\n            .frame(height: infoCapsuleHeight)\n        } else {\n            Color.clear.frame(height: infoCapsuleHeight)\n        }\n    }\n    \n    @ViewBuilder\n    var buttons: some View {\n        HStack {\n            Button(\"Reset\") {\n                assert(!(isReport && Configuration.reportDefault == nil), \"isReport is true but no reportDefault found\")\n                let defaultConfiguration: Configuration = isReport ? .reportDefault ?? .default : .default\n                var newConfiguration = configuration\n                newConfiguration.leading = defaultConfiguration.leading\n                newConfiguration.trailing = defaultConfiguration.trailing\n                newConfiguration.readouts = defaultConfiguration.readouts\n                self.configuration = newConfiguration\n                infoStackAlignment = computeInfoStackAlignment(\n                    infoStackIndex: configuration.leading.count,\n                    totalItems: configuration.all.count\n                )\n                barItems = (configuration.leading + [nil] + configuration.trailing).map { item in\n                    .init(item: item, expanded: true, visible: true)\n                }\n            }\n            .buttonStyle(.plain)\n            .foregroundStyle(.secondary)\n\n            Spacer()\n            \n            Button(\"Apply to All\") { showingApplyToAllConfirmation = true }\n                .confirmationDialog(\n                    \"Really apply this configuration to all interaction bars?\",\n                    isPresented: $showingApplyToAllConfirmation,\n                    titleVisibility: .visible\n                ) {\n                    Button(\"Yes\") {\n                        postInteractionBar = postInteractionBar.applying(other: configuration, types: [.bar])\n                        commentInteractionBar = commentInteractionBar.applying(other: configuration, types: [.bar])\n                        replyInteractionBar = replyInteractionBar.applying(other: configuration, types: [.bar])\n                        // reports intentionally omitted\n                    }\n                }\n        }\n        .padding(.horizontal)\n    }\n    \n    // MARK: - Helpers\n    \n    @ViewBuilder\n    func counterLabel(_ appearance: CounterAppearance) -> some View {\n        let paddingEdges: Edge.Set = {\n            if appearance.leading == nil { return .leading }\n            if appearance.trailing == nil { return .trailing }\n            return []\n        }()\n        \n        HStack(spacing: 0) {\n            if let leading = appearance.leading {\n                InteractionBarActionLabelView(leading)\n            }\n            Text(appearance.value?.description ?? \"\")\n                .monospacedDigit()\n                .foregroundStyle(.themedPrimary)\n                .padding(paddingEdges, Constants.main.standardSpacing)\n            if let trailing = appearance.trailing {\n                InteractionBarActionLabelView(trailing)\n            }\n        }\n        .padding(paddingEdges, 6)\n    }\n    \n    @ViewBuilder\n    func itemLabel(_ item: Configuration.Item?) -> some View {\n        Group {\n            switch item {\n            case let .action(action):\n                InteractionBarActionLabelView(action.appearance)\n            case let .counter(counter):\n                counterLabel(counter.appearance)\n                    .fixedSize()\n            default:\n                infoStack\n                    .frame(maxWidth: .infinity)\n            }\n        }\n        .background {\n            Capsule()\n                .fill(.themedSecondaryGroupedBackground.opacity(0.85))\n        }\n        .geometryGroup()\n    }\n    \n    @ViewBuilder\n    var infoStack: some View {\n        HStack(spacing: 12) {\n            ForEach(configuration.readouts, id: \\.hashValue) { readout in\n                HStack(spacing: 2) {\n                    Image(icon: readout.appearance.icon.representingState(active: false))\n                    Text(readout.appearance.label)\n                }\n                .font(.footnote)\n                .lineLimit(1)\n            }\n        }\n        .foregroundStyle(.themedSecondary)\n        .frame(maxWidth: .infinity, alignment: infoStackAlignment)\n        .padding(Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    func dropIndicator(index: Int) -> some View {\n        Capsule()\n            .fill(.themedAccent)\n            .frame(width: 2, height: 40)\n            .padding(-2)\n            .frame(width: 0)\n            .onAppear {\n                hapticManager.play(haptic: .gentleInfo, tier: .low)\n            }\n    }\n}\n\n// swiftlint:enable file_length\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView.swift",
    "content": "//\n//  InteractionBarEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/08/2024.\n//\n\nimport Flow\nimport Haptics\nimport SwiftUI\nimport Theming\n\nstruct InteractionBarEditorView<Configuration: InteractionBarConfiguration>: View {\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.palette) var palette\n    \n    @Setting(\\.interactionBar_post) var postInteractionBar\n    @Setting(\\.interactionBar_comment) var commentInteractionBar\n    @Setting(\\.interactionBar_reply) var replyInteractionBar\n    \n    @State var configuration: Configuration {\n        didSet {\n            onSet(configuration)\n        }\n    }\n    \n    @State var trayItems: [TrayItem] = .init()\n    @State var barItems: [BarItem] = .init()\n    \n    @State var barPickedUpItem: (barItem: BarItem, index: Int)?\n    @State var trayPickedUpItem: TrayItem?\n    \n    /// Current entity the dragged item is hovered over. -1 indicates the tray.\n    @State var dropLocation: DropLocation?\n    @State var dragLocation: CGPoint = .zero\n    @State var dragTranslation: CGSize = .zero\n    \n    @State var infoStackAlignment: Alignment\n    \n    @State var showingApplyToAllConfirmation: Bool = false\n    \n    let onSet: (Configuration) -> Void\n    let configurationType: ConfigurationType\n    let isReport: Bool\n    \n    let barAnimationDuration: CGFloat = 0.15\n    let trayItemDuration: CGFloat = 0.5\n    \n    @ScaledMetric(relativeTo: .body) var baseInfoCapsuleHeight: CGFloat = 22\n    var infoCapsuleHeight: CGFloat { baseInfoCapsuleHeight + Constants.main.doubleSpacing }\n    \n    init(configuration: Configuration, isReport: Bool, onSet: @escaping (Configuration) -> Void) {\n        self.onSet = onSet\n        self.configuration = configuration\n        self.isReport = isReport\n        let configurationItems: [Configuration.Item?] = configuration.leading + [nil] + configuration.trailing\n        self.configurationType = configuration is PostBarConfiguration ? .post : .comment\n        \n        let newBarItems: [BarItem] = configurationItems.map { .init(item: $0, expanded: true, visible: true) }\n        let newInfoStackIndex = newBarItems.firstIndex(where: { $0.item == nil })\n        assert(newInfoStackIndex != nil, \"could not find infoStack index\")\n        \n        self._barItems = .init(wrappedValue: newBarItems)\n        self._infoStackAlignment = .init(wrappedValue: computeInfoStackAlignment(\n            infoStackIndex: newInfoStackIndex ?? 0,\n            totalItems: newBarItems.count\n        )\n        )\n    }\n    \n    init(setting: ReferenceWritableKeyPath<SettingsValues, Configuration>, isReport: Bool) {\n        self.init(configuration: Settings.get(setting), isReport: isReport) { Settings.set(setting, to: $0) }\n    }\n    \n    var body: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            header\n            buttons\n            Spacer()\n            infoCapsule\n            contentPreview.zIndex(barPickedUpItem == nil ? 0 : 1)\n            Divider()\n            readoutSelectors\n            Divider()\n            tray.zIndex(trayPickedUpItem == nil ? 0 : 1)\n            \n            Button(\"More Widgets...\") {\n                navigation.openSheet(.settings(configuration.widgetPickerPage($configuration)))\n            }\n        }\n        .onChange(of: configuration.availableWidgets, initial: true) {\n            onSet(configuration)\n            trayItems = Configuration.Item.allCases\n                .filter { configuration.availableWidgets.contains($0) }\n                .map { TrayItem(item: $0, visible: true) }\n        }\n        .frame(maxWidth: .infinity)\n        .padding(Constants.main.standardSpacing)\n        .padding(.bottom, Constants.main.standardSpacing)\n        .themedGroupedBackground()\n        .coordinateSpace(.named(\"editor\"))\n        .hiddenNavigationTitle(\"Interaction Bar\")\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment) {\n//        NavigationStack {\n//            InteractionBarEditorView(configuration: PostBarConfiguration.default, isReport: false, onSet: { _ in })\n//        }\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarWidgetPickerView.swift",
    "content": "//\n//  InteractionBarWidgetPickerView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-02-12.\n//\n\nimport ComponentViews\nimport SwiftUI\n\nstruct InteractionBarWidgetPickerView<Configuration: InteractionBarConfiguration>: View {\n    @Environment(\\.dismiss) var dismiss\n    \n    @Binding var configuration: Configuration\n    \n    var body: some View {\n        Form {\n            Section {\n                Text(\"Choose which widgets to display in your palette.\")\n                    .multilineTextAlignment(.center)\n                    .frame(maxWidth: .infinity, alignment: .center)\n            }\n            \n            Section(\"Actions\") {\n                ForEach(Array(Configuration.ActionType.allCases), id: \\.self) { item in\n                    widgetButton(.action(item))\n                }\n            }\n            \n            Section(\"Counters\") {\n                ForEach(Array(Configuration.CounterType.allCases), id: \\.self) { item in\n                    widgetButton(.counter(item))\n                }\n            }\n        }\n        .toolbar {\n            CloseButtonToolbarItem()\n        }\n        .contentMargins(.top, 0)\n    }\n    \n    @ViewBuilder\n    func widgetButton(_ item: Configuration.Item) -> some View {\n        let selected = configuration.availableWidgets.contains(item)\n        let (label, icon): (String, String) = switch item {\n        case let .action(action):\n            (action.appearance.label, action.appearance.barIcon)\n        case let .counter(counter):\n            (.init(localized: counter.appearance.label), counter.appearance.singleIcon)\n        }\n        \n        Button {\n            if selected {\n                configuration.availableWidgets.remove(item)\n            } else {\n                configuration.availableWidgets.insert(item)\n            }\n        } label: {\n            HStack {\n                Label {\n                    Text(label)\n                } icon: {\n                    Image(systemName: icon)\n                        .foregroundStyle(selected ? .themedAccent : .themedSecondary)\n                }\n                \n                Spacer()\n                \n                if selected {\n                    Image(icon: .general.success)\n                        .foregroundStyle(.themedAccent)\n                        .contentTransition(.symbolEffect(.replace, options: .speed(2)))\n                }\n            }\n            .frame(maxWidth: .infinity)\n            .contentShape(.rect)\n        }\n        .buttonStyle(.plain)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/LinkSettingsView.swift",
    "content": "//\n//  LinkSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 27/06/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct LinkSettingsView: View {\n    @Setting(\\.links_openInBrowser) var openLinksInBrowser\n    @Setting(\\.links_readerMode) var openLinksInReaderMode\n    @Setting(\\.links_shareMode) var linkSharingMode\n    @Setting(\\.links_displayMode) var tappableLinksDisplayMode\n    @Setting(\\.comment_compact) var compactComments\n    @Setting(\\.links_embedLoops) var embedLoops\n    @Setting(\\.behavior_autoplayMedia) var autoplayMedia\n    @Setting(\\.behavior_muteVideos) var muteVideos\n\n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Media & Links\",\n                description: \"Manage how Mlem handles links and control how images and videos are displayed.\",\n                icon: .general.image\n            )\n            .gradientTint(.themedColorfulAccent(4))\n            Section {\n                NavigationLink(\n                    \"Open External Links\",\n                    value: .init(localized: externalLinksNavigationLinkValue),\n                    fallbackValue: \"\",\n                    icon: .settings.openExternalLinks,\n                    destination: .settings(.externalLinks)\n                )\n                NavigationLink(\n                    \"Share Links\",\n                    value: .init(localized: sharingLinksNavigationLinkValue),\n                    fallbackValue: \"\",\n                    icon: .general.share,\n                    destination: .settings(.sharingLinks)\n                )\n                NavigationLink(\n                    \"Tappable Links\",\n                    value: tappableLinksDisplayMode == .disabled ? \"Off\" : \"On\",\n                    fallbackValue: \"\",\n                    icon: .settings.tappableLinks,\n                    destination: .settings(.tappableLinks)\n                )\n            }\n\n            Section {\n                NavigationLink(\n                    \"Image Viewer\",\n                    icon: .settings.imageViewer,\n                    destination: .settings(.imageViewer)\n                )\n            }\n            \n            Section {\n                Toggle(\"Autoplay\", icon: .general.playCircle, isOn: $autoplayMedia)\n                Toggle(\"Mute Videos\", icon: .general.muted, isOn: $muteVideos)\n            }\n            \n            Section {\n                NavigationLink(\n                    \"Embedded Content\",\n                    value: embedLoops ? \"On\" : \"Off\",\n                    fallbackValue: \"\",\n                    icon: .general.embedding,\n                    destination: .settings(.embedding)\n                )\n            }\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Media & Links\")\n    }\n    \n    var externalLinksNavigationLinkValue: LocalizedStringResource {\n        if openLinksInBrowser {\n            \"In Browser\"\n        } else {\n            openLinksInReaderMode ? \"In Reader\" : \"In Mlem\"\n        }\n    }\n    \n    var sharingLinksNavigationLinkValue: LocalizedStringResource {\n        switch linkSharingMode {\n        case .myInstance: \"My Instance\"\n        case .originalInstance: \"Original Instance\"\n        case .lemmyverse: \"Universal\"\n        case .askEveryTime: \"Ask Every Time\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/LongPressActionSettingsView.swift",
    "content": "//\n//  LongPressActionSettingsView.swift\n//  Mlem\n//\n//  Created by Bedir Ekim on 21.05.2025.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct LongPressActionSettingsView: View {\n    @Setting(\\.tab_gestures_longPressAction) private var longPressAction: TabBarLongPressAction\n    @Environment(\\.palette) var palette\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Long Press Action\",\n                description: \"Choose which action to perform when you tap and hold the profile icon.\",\n                icon: .settings.longPress\n            )\n            .tint(ThemedColor.themedColorfulAccent(2).gradient(palette: palette))\n            \n            Section {\n                Picker(\"Long Press Action\", selection: $longPressAction) {\n                    ForEach(TabBarLongPressAction.allCases, id: \\.rawValue) { action in\n                        Label(String(localized: action.label), icon: action.icon)\n                            .symbolVariant(.circle)\n                            .tag(action)\n                    }\n                }\n                .labelsHidden()\n                .pickerStyle(.inline)\n            } footer: {\n                Text(\"Swiping up on the tab bar will always open the account switcher.\")\n            }\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Long Press Action\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ModMailInteractionBarSettingsView.swift",
    "content": "//\n//  ModMailInteractionBarSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-06.\n//\n\nimport SwiftUI\n\nstruct ModMailInteractionBarSettingsView: View {\n    @Setting(\\.interactionBar_postReport) var postReportInteractionBar\n    @Setting(\\.interactionBar_commentReport) var commentReportInteractionBar\n    @Setting(\\.interactionBar_alternateReportLayout) var useAlternateLayout\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Mod Mail Action Layouts\",\n                // swiftlint:disable:next line_length\n                description: \"Choose whether to use alternate interaction bar and swipe action layouts for post and comment reports in Mod Mail.\"\n            ) {}\n            Section {\n                Toggle(\"Use Alternate Layouts\", isOn: $useAlternateLayout)\n            }\n            if useAlternateLayout {\n                Section(\"Posts\") {\n                    NavigationLink(.settings(.interactionBar(.postReport))) {\n                        SettingsInteractionBarSummaryView(\n                            title: \"Interaction Bar\",\n                            configuration: postReportInteractionBar\n                        )\n                    }\n                    NavigationLink(\"Swipe Actions\", destination: .settings(.swipeActions(.postReport)))\n                }\n                Section(\"Comments\") {\n                    NavigationLink(.settings(.interactionBar(.commentReport))) {\n                        SettingsInteractionBarSummaryView(\n                            title: \"Interaction Bar\",\n                            configuration: commentReportInteractionBar\n                        )\n                    }\n                    NavigationLink(\"Swipe Actions\", destination: .settings(.swipeActions(.commentReport)))\n                }\n            }\n        }\n        .animation(.easeOut(duration: 0.1), value: useAlternateLayout)\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Mod Mail Action Layouts\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ModeratorActionSeparationSettingsView.swift",
    "content": "//\n//  ModeratorActionSeparationSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-02-01.\n//\n\nimport SwiftUI\n\nstruct ModeratorActionSeparationSettingsView: View {\n    @Setting(\\.menus_modActionGrouping) var moderatorActionGrouping\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Moderator Actions\",\n                description: \"Customize how moderator actions are separated from regular actions in context menus.\"\n            ) {}\n            Section {\n                Picker(\"Separate Actions Using\", icon: .settings.menuItems, selection: $moderatorActionGrouping) {\n                    ForEach(ModeratorActionGrouping.allCases, id: \\.self) { item in\n                        Label(item.label.key, icon: item.icon)\n                    }\n                }\n                .pickerStyle(.inline)\n                .labelsHidden()\n            }\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Moderator Actions\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ModeratorSettingsView.swift",
    "content": "//\n//  ModeratorSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 01/10/2024.\n//\n\nimport Icons\nimport SwiftUI\nimport Theming\n\nstruct ModeratorSettingsView: View {\n    @Setting(\\.menus_modActionGrouping) var moderatorActionGrouping\n    @Setting(\\.menus_allModActions) var showAllModActions\n    @Setting(\\.tab_inbox_badgeIncludedTypes) var tabInboxBadgeIncludedTypes\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Moderation\",\n                description: \"Manage settings related to content moderation.\",\n                icon: .lemmy.moderation\n            )\n            .gradientTint(.themedModeration)\n            Section {\n                NavigationLink(\n                    \"Moderator Actions\",\n                    value: .init(localized: moderatorActionGrouping.label),\n                    fallbackValue: \"\",\n                    icon: .settings.menuItems,\n                    destination: .settings(.separateModeratorActions)\n                )\n            }\n            Section {\n                Toggle(\"Show All Actions in Feed\", icon: .general.menu, isOn: $showAllModActions)\n                    .symbolVariant(.circle)\n            } footer: {\n                Text(\"When disabled, some moderator actions will only be accessible from the post page.\")\n            }\n            Section {\n                NavigationLink(\n                    \"Notification Badge\",\n                    value: tabInboxBadgeIncludedTypes.label(accountType: AccountsTracker.main.highestLevelAccountType),\n                    fallbackValue: .init(localized: \"Some\"),\n                    icon: .settings.unreadBadge,\n                    destination: .settings(.inboxBadge)\n                )\n            }\n            Section {\n                NavigationLink(\n                    \"Mod Mail Action Layouts\",\n                    icon: .settings.interactionBar,\n                    destination: .settings(.modMailInteractionBar)\n                )\n            }\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Moderation\")\n    }\n}\n\nenum ModeratorActionGrouping: String, Codable, CaseIterable {\n    case divider, separateMenu\n    \n    init?(rawValue: String) {\n        switch rawValue {\n        // Decode v1 case\n        case \"none\", \"divider\", \"disclosureGroup\":\n            self = .divider\n        case \"separateMenu\":\n            self = .separateMenu\n        default:\n            return nil\n        }\n    }\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .divider: \"Divider\"\n        case .separateMenu: \"Separate Menu\"\n        }\n    }\n    \n    var icon: Icon {\n        switch self {\n        case .divider: .general.remove\n        case .separateMenu: .lemmy.moderation\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/PostReadIndicatorSettingsView.swift",
    "content": "//\n//  PostReadIndicatorSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-19.\n//\n\nimport ComponentViews\nimport Haptics\nimport SwiftUI\nimport Theming\n\nstruct PostReadIndicatorSettingsView: View {\n    @Environment(HapticManager.self) var hapticManager\n    \n    @Setting(\\.a11y_readPostIndicator) var readPostIndicator\n    @Setting(\\.a11y_readOutlineThickness) var readOutlineThickness\n    \n    @State var readBarThicknessSlider: Double\n    \n    init() {\n        @Setting(\\.a11y_readOutlineThickness) var readOutlineThickness\n        _readBarThicknessSlider = .init(wrappedValue: Double(readOutlineThickness))\n    }\n\n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Read Indicator\",\n                // swiftlint:disable:next line_length\n                description: \"Read posts are shown with dimmed title text. If you like, you can choose an additional way of indicating read status.\",\n                icon: .settings.readIndicatorSetting\n            )\n            .gradientTint(.themedSecondary)\n            Section {\n                Toggle(\n                    \"Additional Read Indicator\",\n                    isOn: .init(\n                        get: { readPostIndicator != .none },\n                        set: { readPostIndicator = $0 ? .checkmark : .none }\n                    )\n                )\n            } footer: {\n                Text(\"This is turned on by default because Differentiate Without Color is enabled in System Settings.\")\n            }\n            if readPostIndicator != .none {\n                Section {\n                    HStack {\n                        pickerItem(for: .checkmark)\n                        pickerItem(for: .outline)\n                    }\n                }\n            }\n            if readPostIndicator == .outline {\n                Section {\n                    outlineThicknessSlider\n                }\n            }\n        }\n        .animation(.easeOut(duration: 0.1), value: readPostIndicator)\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Read Indicator\")\n    }\n    \n    @ViewBuilder\n    var outlineThicknessSlider: some View {\n        VStack(alignment: .leading) {\n            Text(\"Outline Thickness\")\n            \n            Slider(\n                value: $readBarThicknessSlider,\n                in: 1 ... 5,\n                step: 1\n            ) {\n                Text(\"Outline Thickness\")\n            } minimumValueLabel: {\n                Text(verbatim: \"1\")\n            } maximumValueLabel: {\n                Text(verbatim: \"5\")\n            } onEditingChanged: { editing in\n                if !editing {\n                    readOutlineThickness = Int(readBarThicknessSlider)\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func pickerItem(for style: ReadPostIndicator) -> some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            preview(for: style)\n            HStack {\n                Text(style.label)\n                Checkbox(isOn: style == readPostIndicator)\n            }\n        }\n        .frame(maxWidth: .infinity)\n        .onTapGesture {\n            hapticManager.play(haptic: .gentleInfo, tier: .low)\n            readPostIndicator = style\n        }\n    }\n    \n    @ViewBuilder\n    func preview(for style: ReadPostIndicator) -> some View {\n        UnevenRoundedRectangle(\n            cornerRadii: .init(topLeading: 0, bottomLeading: 0, bottomTrailing: 0, topTrailing: 15)\n        )\n        .fill(.themedSecondaryGroupedBackground)\n        .stroke(style == .outline ? .themedSecondary : .clear, lineWidth: 2)\n        .overlay(alignment: .topTrailing) {\n            HStack {\n                if style == .checkmark {\n                    Image(icon: .general.success)\n                        .foregroundStyle(.themedSecondary)\n                }\n                Image(icon: .general.menu)\n            }\n            .font(.title2)\n            .frame(height: 30)\n            .padding(.top, 15)\n            .padding(.trailing, 20)\n        }\n        .padding([.top, .trailing], 20)\n        .padding([.bottom, .leading], -2)\n        .background(.themedGroupedBackground)\n        .frame(width: 120, height: 80)\n        .clipShape(.rect(cornerRadius: 10))\n        .overlay {\n            RoundedRectangle(cornerRadius: 10)\n                .stroke(.themedTertiary, lineWidth: 1)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/PostSettingsView+PostSizePicker.swift",
    "content": "//\n//  PostSettingsView+PostSizePicker.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-18.\n//\n\nimport Flow\nimport SwiftUI\n\nextension PostSettingsView {\n    struct PostSizePicker: View {\n        @Setting(\\.post_size) var postSize\n        \n        var body: some View {\n            Section(\"Size\") {\n                ViewThatFits {\n                    HStack(spacing: 0) {\n                        largeItem\n                        headlineItem\n                        tiledItem\n                        compactItem\n                    }\n                    VStack {\n                        HStack { largeItem; headlineItem }\n                        HStack { tiledItem; compactItem }\n                    }\n                }\n                .listRowInsets(.init(top: 16, leading: 5, bottom: 16, trailing: 5))\n            }\n        }\n        \n        @ViewBuilder var largeItem: some View {\n            DevicePickerItem(PostSize.large.label, item: .large, selected: $postSize) {\n                VStack(spacing: 3) {\n                    ForEach(0 ..< 2) { _ in\n                        RoundedRectangle(cornerRadius: 2)\n                            .overlay {\n                                RoundedRectangle(cornerRadius: 1)\n                                    .opacity(0.5)\n                                    .padding(.horizontal, 3)\n                                    .padding(.top, 8)\n                                    .padding(.bottom, 6)\n                                    .blendMode(.destinationOut)\n                            }\n                            .compositingGroup()\n                            .aspectRatio(3 / 4, contentMode: .fit)\n                    }\n                }\n                .padding(.top, 4)\n            }\n        }\n        \n        @ViewBuilder var headlineItem: some View {\n            DevicePickerItem(PostSize.headline.label, item: .headline, selected: $postSize) {\n                VStack(spacing: 3) {\n                    ForEach(0 ..< 7) { _ in\n                        RoundedRectangle(cornerRadius: 2)\n                            .frame(height: 18)\n                            .overlay(alignment: .topLeading) {\n                                RoundedRectangle(cornerRadius: 1)\n                                    .opacity(0.5)\n                                    .frame(width: 7, height: 7)\n                                    .padding(.top, 5)\n                                    .padding(.leading, 2)\n                                    .blendMode(.destinationOut)\n                            }\n                            .compositingGroup()\n                    }\n                }\n                .padding(.top, 4)\n            }\n        }\n        \n        @ViewBuilder var tiledItem: some View {\n            DevicePickerItem(PostSize.tile.label, item: .tile, selected: $postSize) {\n                VStack(spacing: 3) {\n                    ForEach(0 ..< 5) { _ in\n                        HStack(spacing: 3) {\n                            ForEach(0 ..< 2) { _ in\n                                Rectangle()\n                                    .overlay(alignment: .topLeading) {\n                                        RoundedRectangle(cornerRadius: 1)\n                                            .opacity(0.5)\n                                            .padding(.bottom, 6)\n                                            .blendMode(.destinationOut)\n                                    }\n                                    .clipShape(.rect(cornerRadius: 2))\n                                    .aspectRatio(3 / 4, contentMode: .fit)\n                            }\n                        }\n                    }\n                }\n                .padding(.top, 4)\n            }\n        }\n        \n        @ViewBuilder var compactItem: some View {\n            DevicePickerItem(PostSize.compact.label, item: .compact, selected: $postSize) {\n                VStack(spacing: 3) {\n                    ForEach(0 ..< 7) { _ in\n                        RoundedRectangle(cornerRadius: 2)\n                            .frame(height: 11)\n                            .overlay(alignment: .leading) {\n                                RoundedRectangle(cornerRadius: 1)\n                                    .opacity(0.5)\n                                    .aspectRatio(1, contentMode: .fit)\n                                    .padding(2)\n                                    .blendMode(.destinationOut)\n                            }\n                            .compositingGroup()\n                    }\n                }\n                .padding(.top, 4)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/PostSettingsView.swift",
    "content": "//\n//  PostSettingsView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-27.\n//\n\nimport Foundation\nimport SwiftUI\n\n// note: this is a very lazy categorization of \"properties that affect posts\"\nstruct PostSettingsView: View {\n    @Environment(\\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor: Bool\n    \n    @Setting(\\.post_size) var postSize\n    @Setting(\\.post_allowMultipleColumns) var allowMultipleColumns\n    @Setting(\\.post_thumbnailLocation) var thumbnailLocation\n    @Setting(\\.post_showCreator) var showCreator\n    @Setting(\\.post_showSubscribedStatus) var showSubscribedStatus\n    @Setting(\\.post_showDownvotesCompact) var showDownvotesCompact\n    @Setting(\\.post_gestures_tapToCollapse) var tapPostsToCollapse\n    \n    @Setting(\\.interactionBar_post) var postInteractionBar\n    \n    @Setting(\\.a11y_readPostIndicator) var readPostIndicator\n    \n    var body: some View {\n        Form {\n            PostSizePicker()\n            if UIDevice.isPad {\n                Toggle(\"Multiple Columns\", systemImage: \"square.grid.2x2\", isOn: $allowMultipleColumns)\n            }\n            \n            Section {\n                NavigationLink(.settings(.interactionBar(.post))) {\n                    SettingsInteractionBarSummaryView(configuration: postInteractionBar)\n                }\n                NavigationLink(\"Swipe Actions\", destination: .settings(.swipeActions(.post)))\n            }\n            \n            Section {\n                NavigationLink(\n                    \"Subscription Indicator\",\n                    value: showSubscribedStatus ? .init(localized: \"On\") : .init(localized: \"Off\"),\n                    fallbackValue: \"\",\n                    icon: .lemmy.subscribedFeed,\n                    destination: .settings(.postSubscriptionIndicator)\n                )\n                \n                if postSize == .headline || postSize == .compact {\n                    NavigationLink(\n                        \"Thumbnail\",\n                        value: .init(localized: thumbnailLocation.label),\n                        fallbackValue: \"\",\n                        icon: .settings.thumbnail,\n                        destination: .settings(.postThumbnail)\n                    )\n                }\n                \n                if postSize == .compact {\n                    Toggle(\"Show Downvotes Separately\", icon: .lemmy.votes, isOn: $showDownvotesCompact)\n                }\n                \n                if differentiateWithoutColor {\n                    NavigationLink(\n                        \"Read Indicator\",\n                        value: .init(localized: readPostIndicator.label),\n                        fallbackValue: \"\",\n                        icon: .settings.readIndicatorSetting,\n                        destination: .settings(.postReadIndicator)\n                    )\n                }\n            }\n            \n            Section {\n                Toggle(\"Tap to Collapse\", icon: .general.collapse, isOn: $tapPostsToCollapse)\n            }\n            \n            if postSize != .tile, postSize != .compact {\n                Section {\n                    Toggle(\"Always Show Usernames\", icon: .settings.author, isOn: $showCreator)\n                }\n            }\n        }\n        .withConditionalLabelStyle()\n        .navigationTitle(\"Posts\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/PostSubscriptionIndicatorSettingsView.swift",
    "content": "//\n//  PostSubscriptionIndicatorSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-18.\n//\n\nimport SwiftUI\n\nstruct PostSubscriptionIndicatorSettingsView: View {\n    @Environment(\\.colorScheme) var colorScheme\n    @Environment(\\.palette) var palette\n    \n    @Setting(\\.post_showSubscribedStatus) var showSubscribedStatus\n    \n    var body: some View {\n        Form {\n            previewSection\n            Section {\n                Toggle(\"Subscription Indicator\", isOn: $showSubscribedStatus)\n            }\n        }\n        .contentMargins(.top, 16)\n        .navigationTitle(\"Subscription Indicator\")\n    }\n    \n    @ViewBuilder\n    var previewSection: some View {\n        Section {\n            UnevenRoundedRectangle(\n                cornerRadii: .init(topLeading: 16, bottomLeading: 0, bottomTrailing: 10, topTrailing: 0)\n            )\n            .fill(.themedTertiaryGroupedBackground)\n            .strokeBorder(colorScheme == .light ? .themedSecondaryGroupedBackground : .clear, lineWidth: 2)\n            .frame(height: 100)\n            .overlay(alignment: .topLeading) {\n                HStack(spacing: 0) {\n                    CircleCroppedImageView(url: nil, frame: 30, fallback: .communityAvatar)\n                        .opacity(0.8)\n                    Circle()\n                        .fill(.themedSecondary)\n                        .frame(width: showSubscribedStatus ? 10 : 0, height: 10)\n                        .opacity(showSubscribedStatus ? 10 : 0)\n                        .padding(.leading, showSubscribedStatus ? 12 : 5)\n                        .padding(.trailing, showSubscribedStatus ? 10 : 5)\n                    labelText\n                        .lineLimit(1)\n                        .fixedSize(horizontal: false, vertical: true)\n                        .font(.title2)\n                        .foregroundStyle(.themedSecondary)\n                        .opacity(0.8)\n                        .mask {\n                            LinearGradient(colors: [.black, .black.opacity(0.5)], startPoint: .leading, endPoint: .trailing)\n                        }\n                        .offset(y: -1)\n                }\n                .padding([.top, .leading], 20)\n                .animation(.bouncy, value: showSubscribedStatus)\n            }\n            .padding([.top, .leading], 20)\n            .listRowInsets(.init())\n        }\n    }\n    \n    var labelText: Text {\n        let string = String(localized: \"news@example.com\")\n        let parts = string.split(separator: \"@\")\n        guard parts.count == 2 else {\n            assertionFailure()\n            return Text(string)\n        }\n        return Text(parts[0]) + Text(verbatim: \"@\\(parts[1])\").foregroundColor(palette.label.tertiary)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/PostThumbnailSettingsView.swift",
    "content": "//\n//  PostThumbnailSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-18.\n//\n\nimport ComponentViews\nimport SwiftUI\n\nstruct PostThumbnailSettingsView: View {\n    @Setting(\\.post_thumbnailLocation) var thumbnailLocation\n    @Setting(\\.a11y_websiteThumbnailIcon) var websiteThumbnailIcon\n    \n    // capsule color gradient configuration\n    let gradientBegin: CGFloat = 0.55\n    let gradientEnd: CGFloat = 0.45\n    \n    var body: some View {\n        Form {\n            Section {\n                alignmentPreview(location: thumbnailLocation)\n                    .animation(.easeInOut(duration: 0.2), value: thumbnailLocation)\n            }\n            \n            Section {\n                Picker(\"Thumbnail Location\", selection: $thumbnailLocation) {\n                    ForEach(ThumbnailLocation.allCases, id: \\.self) { location in\n                        Label(location.label.key, icon: location.icon)\n                            .tag(location)\n                    }\n                }\n                .labelsHidden()\n                .pickerStyle(.inline)\n            }\n            \n            Section {\n                Toggle(\"Website Icon\", icon: .general.browser, isOn: $websiteThumbnailIcon)\n            } footer: {\n                Text(\"Indicate link thumbnails with an icon.\")\n            }\n        }\n        .withConditionalLabelStyle()\n        .navigationTitle(\"Thumbnail\")\n    }\n    \n    @ViewBuilder\n    func alignmentPreview(location: ThumbnailLocation) -> some View {\n        HStack(spacing: 8) {\n            thumbnailView(active: location == .left)\n            \n            GeometryReader { geometry in\n                VStack(alignment: .leading, spacing: 5) {\n                    MockTextView()\n                        .frame(width: geometry.size.width / 2, height: geometry.size.height / 6)\n                    MockTextView(beginOpacity: 0.65, endOpacity: 0.55)\n                        .frame(width: geometry.size.width * 4 / 5, height: geometry.size.height / 4)\n                    MockTextView()\n                        .frame(width: geometry.size.width / 3, height: geometry.size.height / 6)\n                }\n                .foregroundStyle(.themedSecondary)\n                .frame(maxWidth: .infinity, alignment: .leading)\n                .padding(.top, 2)\n            }\n            .frame(maxWidth: .infinity, maxHeight: .infinity)\n            .padding(.leading, location == .left ? 0 : -8)\n            \n            thumbnailView(active: location == .right)\n        }\n        .aspectRatio(8 / 2, contentMode: .fit)\n        .padding(8)\n        .background(.themedTertiaryGroupedBackground, in: .rect(cornerRadius: Constants.main.mediumItemCornerRadius))\n    }\n    \n    @ViewBuilder\n    func thumbnailView(active: Bool) -> some View {\n        RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius)\n            .fill(.themedAccent.opacity(0.6))\n            .frame(maxHeight: .infinity)\n            .aspectRatio(.init(width: active ? 1 : 0, height: 1), contentMode: .fit)\n            .overlay {\n                Image(systemName: \"mountain.2.fill\")\n                    .font(.system(size: 30))\n                    .foregroundStyle(.white)\n                    .opacity(active ? 0.9 : 0)\n            }\n            .overlay {\n                Image(icon: .general.browser)\n                    .resizable()\n                    .frame(width: 20, height: 20)\n                    .foregroundStyle(.white)\n                    .background(.ultraThinMaterial, in: .circle)\n                    .padding(6)\n                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)\n                    .opacity(websiteThumbnailIcon ? 1 : 0)\n                    .opacity(active ? 1 : 0)\n                    .animation(.easeIn(duration: 0.2), value: websiteThumbnailIcon)\n            }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/PrivacyBypassImageProxySettingsView.swift",
    "content": "//\n//  PrivacyBypassImageProxySettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-25.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct PrivacyBypassImageProxySettingsView: View {\n    @Setting(\\.privacy_autoBypassImageProxy) var bypassImageProxy\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Bypass Image Proxy\",\n                // swiftlint:disable:next line_length\n                description: \"Some instances proxy images to protect your privacy. In certain cases, this causes image loading to fail. You can bypass the image proxy and load directly, but this will expose your IP address to the image host.\",\n                icon: .lemmy.imageProxy\n            )\n            .gradientTint(.themedColorfulAccent(4))\n            Section(\"Bypass Image Proxy...\") {\n                Picker(\"Bypass Image Proxy\", selection: $bypassImageProxy) {\n                    Label(\"Automatically\", icon: .general.success)\n                        .symbolVariant(.circle)\n                        .tag(true)\n                    Label(\"Ask First\", systemImage: \"questionmark.circle\")\n                        .tag(false)\n                }\n                .pickerStyle(.inline)\n                .labelsHidden()\n            }\n        }\n        .contentMargins(.top, 16)\n        .withConditionalLabelStyle()\n        .hiddenNavigationTitle(\"Bypass Image Proxy\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/PrivacySettingsView.swift",
    "content": "//\n//  PrivacySettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-25.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct PrivacySettingsView: View {\n    @Setting(\\.privacy_autoBypassImageProxy) var bypassImageProxy\n    @Setting(\\.behavior_confirmImageUploads) var confirmImageUploads\n    @Setting(\\.post_webPreview_showIcon) var showFavicons\n\n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Privacy\",\n                description: \"Manage how Mlem interacts with Lemmy instances and other websites.\",\n                icon: .settings.privacy\n            )\n            .gradientTint(.themedColorfulAccent(2))\n            Section {\n                Toggle(\"Confirm Image Uploads\", icon: .settings.confirmImageUploads, isOn: $confirmImageUploads)\n            } footer: {\n                Text(\"When enabled, Mlem will ask you to confirm your choice before uploading an image to your instance.\")\n            }\n            Section {\n                NavigationLink(\n                    \"Bypass Image Proxy\",\n                    value: .init(localized: bypassImageProxyNavigationLinkValue),\n                    fallbackValue: \"\",\n                    icon: .lemmy.imageProxy,\n                    destination: .settings(.privacyBypassImageProxy)\n                )\n            }\n            Section {\n                Toggle(\"Hide Website Icons\", systemImage: \"camera.macro.circle\", isOn: $showFavicons.invert())\n            } footer: {\n                // swiftlint:disable:next line_length\n                Text(\"Mlem uses a Google API to fetch website icon URLs. If you'd prefer not to use this, you can choose to hide website icons.\")\n            }\n            Section {\n                NavigationLink(\n                    \"Mlem Privacy Policy\",\n                    icon: .settings.privacy,\n                    destination: .settings(.document(.privacyPolicy))\n                )\n            }\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Privacy\")\n    }\n    \n    var bypassImageProxyNavigationLinkValue: LocalizedStringResource {\n        bypassImageProxy ? \"Automatically\" : \"Ask First\"\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ProfileSettingsView.swift",
    "content": "//\n//  ProfileSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-03.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ProfileSettingsView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    @Environment(\\.palette) var palette\n    @Environment(\\.colorScheme) var colorScheme\n    @Environment(\\.dismiss) var dismiss\n    \n    let person: Person\n\n    @State var profileDetails: ProfileDetails\n    \n    @State var bioTextView: UITextView = .init()\n    @State var markdownToolbarEditorModel: MarkdownEditorToolbarModel = .init()\n    @State var uploadHistory: ImageUploadHistoryManager = .init()\n    \n    @State var avatarManager: ImageUploadManager = .init()\n    @State var bannerManager: ImageUploadManager = .init()\n    \n    @State var isSubmitting: Bool = false\n    \n    init(person: Person) {\n        self.person = person\n        self._profileDetails = .init(wrappedValue: person.profileDetails())\n        bioTextView.text = person.description ?? \"\"\n    }\n\n    var displayNameText: Binding<String> {\n        .init(get: {\n            profileDetails.displayName ?? \"\"\n        }, set: { newValue in\n            if newValue == person.displayName || newValue.isEmpty {\n                profileDetails.displayName = nil\n            }\n        })\n    }\n    \n    var minTextEditorHeight: CGFloat {\n        UIFont.preferredFont(forTextStyle: .body).lineHeight * 6 + 20\n    }\n    \n    var body: some View {\n        Form {\n            if person.api.supports(.editDisplayName, defaultValue: true) {\n                displayNameSection\n            }\n            Section(\"Biography\") {\n                MarkdownTextEditor(\n                    onChange: { profileDetails.description = $0 },\n                    prompt: \"Write a bit about yourself...\",\n                    textView: bioTextView,\n                    insets: .init(\n                        top: Constants.main.standardSpacing,\n                        left: Constants.main.standardSpacing,\n                        bottom: Constants.main.standardSpacing,\n                        right: Constants.main.standardSpacing\n                    ),\n                    firstResponder: false,\n                    sizingOffset: 10,\n                    content: {\n                        MarkdownEditorToolbarView(\n                            textView: bioTextView,\n                            uploadHistory: uploadHistory,\n                            model: markdownToolbarEditorModel\n                        )\n                    }\n                )\n                .frame(\n                    maxWidth: .infinity,\n                    minHeight: minTextEditorHeight,\n                    maxHeight: .infinity,\n                    alignment: .topLeading\n                )\n                .listRowInsets(.init())\n            }\n            avatarSection\n            bannerSection\n        }\n        .onAppear {\n            markdownToolbarEditorModel.imageUploadApi = person.api\n        }\n        .navigationTitle(\"My Profile\")\n        .navigationBarTitleDisplayMode(.inline)\n        .scrollDismissesKeyboard(.interactively)\n        .navigationBarBackButtonHidden(showToolbarOptions)\n        .interactiveDismissDisabled(showToolbarOptions)\n        .toolbar {\n            if showToolbarOptions {\n                ToolbarItem(placement: .topBarLeading) {\n                    Button {\n                        profileDetails = person.profileDetails()\n                        bioTextView.text = profileDetails.description\n                    } label: {\n                        if #available(iOS 26, *) {\n                            Label(\"Discard\", icon: .general.delete)\n                        } else {\n                            Text(\"Cancel\")\n                        }\n                    }\n                    .disabled(isSubmitting)\n                }\n                ToolbarItem(placement: .topBarTrailing) {\n                    if isSubmitting {\n                        ProgressView()\n                    } else {\n                        saveButtonView\n                    }\n                }\n            } else if navigation.isInsideSheet {\n                CloseButtonToolbarItem()\n            }\n        }\n    }\n    \n    var showToolbarOptions: Bool { profileDetails != person.profileDetails() }\n\n    @ViewBuilder\n    var displayNameSection: some View {\n        Section(\"Display Name\") {\n            TextField(\"Display Name\", text: displayNameText, prompt: Text(person.name))\n                .autocorrectionDisabled()\n                .textInputAutocapitalization(.never)\n        } footer: {\n            Text(\"The name that is displayed on your profile. This is not the same as your username, which cannot be changed.\")\n        }\n    }\n    \n    @ViewBuilder\n    var avatarSection: some View {\n        Section {\n            HStack(spacing: 15) {\n                CircleCroppedImageView(url: profileDetails.avatar, frame: 48, fallback: .personAvatar)\n                Text(\"Avatar\")\n                Spacer()\n                CircleImageUploadButton(imageManager: avatarManager, url: $profileDetails.avatar, api: person.api)\n            }\n            .onChange(of: avatarManager.image?.url) {\n                profileDetails.avatar = avatarManager.image?.url\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var bannerSection: some View {\n        Section {\n            VStack(spacing: 0) {\n                if let bannerUrl = profileDetails.banner {\n                    MediaView(\n                        url: bannerUrl,\n                        contentMode: .fill,\n                        enableContextMenu: true,\n                        enableImageViewer: true\n                    )\n                    .frame(height: 150)\n                    .clipped()\n                } else {\n                    palette.label.secondary.opacity(0.5)\n                        .frame(height: 150)\n                }\n                HStack(spacing: 15) {\n                    Text(\"Banner\")\n                    Spacer()\n                    CircleImageUploadButton(imageManager: bannerManager, url: $profileDetails.banner, api: person.api)\n                }\n                .padding(.horizontal, 15)\n                .padding(.vertical, 10)\n            }\n            .onChange(of: bannerManager.image?.url) {\n                profileDetails.banner = bannerManager.image?.url\n            }\n        }\n        .listRowInsets(.init())\n    }\n    \n    @ViewBuilder\n    var saveButtonView: some View {\n        Button {\n            Task { @MainActor in await submit() }\n        } label: {\n            if #available(iOS 26, *) {\n                Label(\"Save\", icon: .general.success)\n            } else {\n                Text(\"Save\")\n            }\n        }\n        .glassProminentButtonStyle()\n    }\n    \n    @MainActor\n    func submit() async {\n        isSubmitting = true\n        do {\n            try await person.updateProfile(profileDetails)\n            if let session = appState.firstSession as? UserSession, session.person === person {\n                try await session.updateAccount()\n            } else {\n                assertionFailure()\n            }\n            dismiss()\n        } catch {\n            handleError(error)\n        }\n        isSubmitting = false\n    }\n}\n\nprivate struct CircleImageUploadButton: View {\n    let imageManager: ImageUploadManager\n    @Binding var url: URL?\n    let api: ApiClient\n    \n    var body: some View {\n        Group {\n            if url != nil {\n                Button {\n                    url = nil\n                } label: {\n                    Image(icon: .general.delete)\n                        .resizable()\n                        .symbolVariant(.circle.fill)\n                }\n            } else {\n                switch imageManager.state {\n                case .uploading:\n                    ProgressView()\n                        .controlSize(.extraLarge)\n                default:\n                    ImageUploadMenu(imageManager: imageManager, imageUploadApi: api) {\n                        Image(icon: .general.add)\n                            .resizable()\n                            .symbolVariant(.circle.fill)\n                    }\n                }\n            }\n        }\n        .aspectRatio(contentMode: .fit)\n        .frame(height: 36)\n        .symbolRenderingMode(.hierarchical)\n        .fontWeight(.regular)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/SafetyBlurNsfwSettingsView.swift",
    "content": "//\n//  SafetyBlurNsfwSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-24.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct SafetyBlurNsfwSettingsView: View {\n    @Setting(\\.safety_blurNsfw) var blurNsfw\n    \n    var body: some View {\n        Form {\n            headerView\n            Picker(\"Blur NSFW Content\", selection: $blurNsfw) {\n                ForEach(NsfwBlurBehavior.allCases, id: \\.self) { type in\n                    Label(String(localized: type.label), icon: type.icon)\n                        .symbolVariant(.circle)\n                }\n            }\n            .pickerStyle(.inline)\n            .labelsHidden()\n        }\n        .contentMargins(.top, 16)\n        .withConditionalLabelStyle()\n        .hiddenNavigationTitle(\"Blur NSFW Content\")\n    }\n    \n    @ViewBuilder\n    var headerView: some View {\n        SettingsHeaderView(\n            title: \"Blur NSFW Content\",\n            description: \"Choose when Not Safe For Work content should be blurred.\",\n            icon: .general.hide\n        )\n        .gradientTint(.themedWarning)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/SafetySettingsView.swift",
    "content": "//\n//  SafetySettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-24.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct SafetySettingsView: View {\n    @Environment(FiltersTracker.self) var filtersTracker\n\n    @Setting(\\.safety_blurNsfw) var blurNsfw\n    @Setting(\\.filters_keywordFilterEnabled) var keywordFilterEnabled\n    @Setting(\\.safety_enableNsfwCommunityWarning) var showNsfwCommunityWarning\n    @Setting(\\.safety_enableModlogWarning) var showModlogWarning\n\n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Safety & Filtering\",\n                // swiftlint:disable:next line_length\n                description: \"Customize how content is displayed in your feed. Choose which types of content are blurred, and apply filters to hide posts from the feed altogether.\",\n                icon: .settings.safety\n            )\n            .gradientTint(.themedColorfulAccent(3))\n            Section {\n                NavigationLink(\n                    \"Blur NSFW Content\",\n                    value: .init(localized: blurNsfw.label),\n                    fallbackValue: \"\",\n                    icon: .settings.blurNsfw,\n                    destination: .settings(.safetyBlurNsfw)\n                )\n                NavigationLink(\n                    \"Content Warnings\",\n                    value: String(localized: contentWarningsNavigationLinkValue),\n                    fallbackValue: \"\",\n                    icon: .general.warning,\n                    destination: .settings(.safetyWarnings)\n                )\n            }\n            Section {\n                NavigationLink(\n                    \"Filters\",\n                    value: .init(localized: filtersNavigationLinkValue),\n                    fallbackValue: \"\",\n                    icon: .settings.keywordFilter,\n                    destination: .settings(.filters)\n                )\n            }\n        }\n        .contentMargins(.top, 16)\n        .withConditionalLabelStyle()\n        .hiddenNavigationTitle(\"Safety & Filtering\")\n    }\n    \n    var contentWarningsNavigationLinkValue: LocalizedStringResource {\n        switch (showNsfwCommunityWarning, showModlogWarning) {\n        case (true, true): \"All\"\n        case (true, false): \"NSFW Communities\"\n        case (false, true): \"Modlogs\"\n        case (false, false): \"None\"\n        }\n    }\n    \n    var filtersNavigationLinkValue: LocalizedStringResource {\n        var sum = 0\n        if filtersTracker.keywordFilterEnabled && !filtersTracker.rawKeywords.isEmpty { sum += 1 }\n        if filtersTracker.literalFilterEnabled && !filtersTracker.literals.isEmpty { sum += 1 }\n        return sum > 0 ? \"\\(sum) Active\" : \"None\"\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/SafetyWarningsSettingsView.swift",
    "content": "//\n//  SafetyWarningsSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-24.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct SafetyWarningsSettingsView: View {\n    @Setting(\\.safety_enableNsfwCommunityWarning) var showNsfwCommunityWarning\n    @Setting(\\.safety_enableModlogWarning) var showModlogWarning\n\n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Content Warnings\",\n                description: \"Choose whether to show a warning when opening a page that is likely to contain sensitive content.\",\n                icon: .general.warning\n            )\n            .gradientTint(.themedWarning)\n            Section(\"Show warnings when opening...\") {\n                Toggle(\"NSFW Communities\", icon: .lemmy.community, isOn: $showNsfwCommunityWarning)\n                Toggle(\"Modlogs\", icon: .lemmy.modlog, isOn: $showModlogWarning)\n            }\n        }\n        .contentMargins(.top, 16)\n        .withConditionalLabelStyle()\n        .hiddenNavigationTitle(\"Content Warnings\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/SettingsView.swift",
    "content": "//\n//  SettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 07/05/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct SettingsView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    @Setting(\\.appearance_palette) var colorPalette\n    \n    @State var currentIcon: String? = UIApplication.shared.alternateIconName\n\n    var accounts: [UserAccount] { AccountsTracker.main.userAccounts }\n    \n    var body: some View {\n        Form {\n            Section {\n                accountSettingsLink\n                accountListLink\n            }\n            Section {\n                NavigationLink(\n                    \"General\",\n                    icon: .settings.general,\n                    destination: .settings(.general)\n                )\n                .gradientTint(.themedNeutralAccent)\n                NavigationLink(\n                    \"Privacy\",\n                    icon: .settings.privacy,\n                    destination: .settings(.privacy)\n                )\n                .gradientTint(.themedColorfulAccent(2))\n                NavigationLink(\n                    \"Safety & Filtering\",\n                    icon: .settings.safety,\n                    destination: .settings(.safety)\n                )\n                .gradientTint(.themedColorfulAccent(3))\n                NavigationLink(\n                    \"Accessibility\",\n                    icon: .settings.accessibility,\n                    destination: .settings(.accessibility)\n                )\n                .gradientTint(.themedColorfulAccent(2))\n                NavigationLink(\n                    \"Media & Links\",\n                    icon: .general.image,\n                    destination: .settings(.links)\n                )\n                .gradientTint(.themedColorfulAccent(4))\n                NavigationLink(\n                    \"Sorting\",\n                    icon: .settings.sorting,\n                    destination: .settings(.sorting)\n                )\n                .gradientTint(.themedColorfulAccent(5))\n                if AccountsTracker.main.highestLevelAccountType >= .moderator {\n                    NavigationLink(\n                        \"Moderation\",\n                        icon: .lemmy.moderation,\n                        destination: .settings(.moderation)\n                    )\n                    .gradientTint(.themedModeration)\n                    .symbolVariant(.fill)\n                }\n            }\n            \n            Section {\n                appIconSettingsLink\n                NavigationLink(.settings(.theme)) {\n                    ThemeLabel(title: \"Theme\", palette: colorPalette)\n                }\n                .labelStyle(.automatic)\n            }\n            \n            Section {\n                NavigationLink(\"Posts\", icon: .lemmy.post, destination: .settings(.post))\n                    .gradientTint(.themedPostAccent)\n                NavigationLink(\"Comments\", icon: .lemmy.comment, destination: .settings(.comment))\n                    .gradientTint(.themedCommentAccent)\n                NavigationLink(\"Inbox\", icon: .lemmy.inbox, destination: .settings(.inbox))\n                    .gradientTint(.themedInbox)\n                NavigationLink(\"Communities\", icon: .lemmy.community, destination: .settings(.community))\n                    .gradientTint(.themedCommunityAccent)\n                NavigationLink(\"Tab Bar\", icon: .settings.tabBar, destination: .settings(.tabBar))\n                    .gradientTint(.themedColorfulAccent(5))\n            }\n            \n            Section {\n                NavigationLink(\"About Mlem\", icon: .general.info, destination: .settings(.about))\n                    .gradientTint(.themedColorfulAccent(2))\n                NavigationLink(\"Advanced\", icon: .settings.advanced, destination: .settings(.advanced))\n                    .gradientTint(.themedNeutralAccent)\n            }\n        }\n        .labelStyle(.squircle)\n        .navigationTitle(\"Settings\")\n        .navigationBarTitleDisplayMode(.inline)\n    }\n    \n    @ViewBuilder\n    var accountSettingsLink: some View {\n        NavigationLink(.settings(.account)) {\n            let account = appState.firstSession\n            HStack(spacing: 23) {\n                CircleCroppedImageView(account.account, frame: 54)\n                    .padding(.vertical, -6)\n                    .padding(.leading, 3)\n                VStack(alignment: .leading, spacing: 3) {\n                    Text(account is UserSession ? account.account.nickname : \"Guest\")\n                        .font(.title2)\n                    Text(accountSettingsLinkSubtitle)\n                        .foregroundStyle(.secondary)\n                        .font(.caption)\n                }\n                Spacer()\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var appIconSettingsLink: some View {\n        NavigationLink(.settings(.icon)) {\n            Label {\n                Text(\"App Icon\")\n            } icon: {\n                let icon = AlternateIcon(id: currentIcon, name: String(\"\"))\n                AlternateIconLabel(icon: icon, selected: true).getImage()\n                    .resizable()\n                    .scaledToFit()\n                    .frame(width: Constants.main.settingsIconSize, height: Constants.main.settingsIconSize)\n                    .cornerRadius(Constants.main.smallItemCornerRadius)\n            }\n        }\n        .onChange(of: UIApplication.shared.alternateIconName) {\n            currentIcon = UIApplication.shared.alternateIconName\n        }\n    }\n    \n    var accountSettingsLinkSubtitle: String { \"@\\(appState.firstSession.account.host)\" }\n    \n    @ViewBuilder\n    var accountListLink: some View {\n        NavigationLink(.settings(.accounts)) {\n            HStack(spacing: 10) {\n                AvatarStackView(\n                    urls: accounts.prefix(4).map(\\.avatar),\n                    fallback: .personAvatar,\n                    height: 28,\n                    spacing: accounts.count <= 3 ? 18 : 14,\n                    outlineWidth: 0.7,\n                    showPlusIcon: accounts.count == 1\n                )\n                .frame(height: 28)\n                .frame(minWidth: 80)\n                .padding(.leading, -10)\n                Text(\"Accounts\")\n                Spacer()\n                Text(String(accounts.count))\n                    .foregroundStyle(.secondary)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/SharingLinksSettingsView.swift",
    "content": "//\n//  SharingLinksSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-09.\n//\n\nimport ComponentViews\nimport Icons\nimport SwiftUI\nimport Theming\n\nstruct SharingLinksSettingsView: View {\n    @Setting(\\.links_shareMode) var linkSharingMode\n    @Setting(\\.a11y_showSettingsIcons) var showSettingsIcons\n\n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Share Links\",\n                // swiftlint:disable:next line_length\n                description: \"In the Fediverse, many different links can point to the same piece of content. Choose which site to use when sharing content.\",\n                icon: .general.share\n            )\n            .gradientTint(.themedColorfulAccent(3))\n            \n            pickerItemView(\n                mode: .myInstance,\n                title: \"My Instance\",\n                description: \"Share links using the instance you are currently connected to.\",\n                icon: .lemmy.instance\n            )\n            \n            pickerItemView(\n                mode: .originalInstance,\n                title: \"Original Instance\",\n                description: \"Share links using the instance that the content originated from.\",\n                icon: .settings.author\n            )\n\n            pickerItemView(\n                mode: .lemmyverse,\n                title: \"Universal Link\",\n                description: \"Share links using \\(\"https://lemmyverse.link\"). When someone opens the link, they can choose which instance to use.\",\n                icon: .general.website\n            )\n            pickerItemView(\n                mode: .askEveryTime,\n                title: \"Ask Every Time\",\n                description: \"Every time I share a link, show a popup asking which instance to use.\",\n                icon: .settings.ask\n            )\n        }\n        .contentMargins(.top, 16)\n        .withConditionalLabelStyle()\n        .animation(.easeInOut(duration: 0.1), value: linkSharingMode)\n        .hiddenNavigationTitle(\"Share Links\")\n    }\n    \n    @ViewBuilder\n    func pickerItemView(\n        mode: LinkSharingMode,\n        title: LocalizedStringResource,\n        description: LocalizedStringResource,\n        icon: Icon\n    ) -> some View {\n        HStack(alignment: .top) {\n            if showSettingsIcons {\n                Image(icon: icon)\n                    .foregroundStyle(.themedAccent)\n                    .frame(width: 30)\n                    .padding(.top, 2)\n            }\n            VStack(alignment: .leading) {\n                Text(title)\n                Text(description)\n                    .font(.footnote)\n                    .foregroundStyle(.themedSecondary)\n            }\n            .frame(maxWidth: .infinity, alignment: .leading)\n            Checkbox(isOn: linkSharingMode == mode)\n        }\n        .contentShape(.rect)\n        .onTapGesture {\n            linkSharingMode = mode\n        }\n        .listRowInsets(.init(top: 10, leading: showSettingsIcons ? 10 : 16, bottom: 10, trailing: 16))\n    }\n}\n\nenum LinkSharingMode: String, Codable, CaseIterable {\n    case myInstance, originalInstance, lemmyverse, askEveryTime\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/SortingSettingsView.swift",
    "content": "//\n//  SortingSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 10/08/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct SortingSettingsView: View {\n    @Setting(\\.post_defaultSort) var legacyDefaultPostSort\n    @Setting(\\.post_fallbackSort) var legacyFallbackPostSort\n    @Setting(\\.comment_defaultSort) var legacyDefaultCommentSort\n    \n    var defaultPostSort: PostSortType {\n        get { .init(legacyDefaultPostSort) }\n        nonmutating set { legacyDefaultPostSort = newValue.v3ApiType ?? .hot }\n    }\n    \n    var fallbackPostSort: PostSortType {\n        get { .init(legacyFallbackPostSort) }\n        nonmutating set { legacyFallbackPostSort = newValue.v3ApiType ?? .hot }\n    }\n    \n    var defaultCommentSort: CommentSortType {\n        get { .init(legacyDefaultCommentSort) }\n        nonmutating set { legacyDefaultCommentSort = newValue.v3CommentApiType }\n    }\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Sorting\",\n                description: \"Choose the default sort mode for posts and comments.\",\n                icon: .settings.sorting\n            )\n            .gradientTint(.themedColorfulAccent(5))\n            Section {\n                HStack {\n                    Text(\"Posts\")\n                    Spacer()\n                    FeedSortPicker(sort: .init(\n                        get: { defaultPostSort }, set: { defaultPostSort = $0 }\n                    ))\n                    .foregroundStyle(.themedAccent)\n                    .frame(minHeight: 50)\n                    .buttonStyle(.bordered)\n                }\n                if !defaultPostSort.supportedByAllSoftwares {\n                    HStack {\n                        Text(\"Fallback\")\n                        Spacer()\n                        FeedSortPicker(sort: .init(\n                            get: { fallbackPostSort }, set: { fallbackPostSort = $0 }\n                        ))\n                        .foregroundStyle(.themedAccent)\n                        .frame(minHeight: 50)\n                        .buttonStyle(.bordered)\n                    }\n                }\n            } footer: {\n                if !defaultPostSort.supportedByAllSoftwares {\n                    // swiftlint:disable:next line_length\n                    Text(\"The \\\"\\(defaultPostSort.label())\\\" sort mode is only available on some instances. On unsupported instances, the \\\"Fallback\\\" sort mode will be used instead.\")\n                }\n            }\n            \n            Section {\n                HStack {\n                    Text(\"Comments\")\n                    Spacer()\n                    Menu(defaultCommentSort.label(timeRangeFormat: .topOnly), icon: defaultCommentSort.icon) {\n                        Picker(\"Sort\", selection: .init(get: { defaultCommentSort }, set: { defaultCommentSort = $0 })) {\n                            ForEach(CommentSortType.legacyCases, id: \\.self) { item in\n                                Label(item.label(timeRangeFormat: .topOnly), icon: item.icon)\n                            }\n                        }\n                    }\n                    .foregroundStyle(.themedAccent)\n                    .frame(minHeight: 50)\n                    .buttonStyle(.bordered)\n                }\n            }\n        }\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Sorting\")\n    }\n}\n\nprivate extension PostSortType {\n    var supportedByAllSoftwares: Bool {\n        // This assumes that sort types won't be *removed* once they are added.\n        // This is fine for now but may need updating in future\n        SiteSoftwareType.allCases.allSatisfy { softwareType in\n            SiteSoftware(type: softwareType, version: softwareType.minimumSupportedVersion)\n                .supports(.postSortType(self))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/SubscriptionListSettingsView.swift",
    "content": "//\n//  SubscriptionListSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 23/06/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct SubscriptionListSettingsView: View {\n    @Setting(\\.subscriptions_sort) private var sort\n    @Setting(\\.subscriptions_instanceLocation) var instanceLocation\n    @Setting(\\.navigation_sidebarVisibleByDefault) var sidebarVisibleByDefault\n\n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Subscription List\",\n                description: \"Customize how your subscription list is sorted.\",\n                icon: .lemmy.subscriptionList\n            )\n            .gradientTint(.themedColorfulAccent(4))\n            Section(\"Sort by...\") {\n                Picker(\"Sort by...\", selection: $sort) {\n                    ForEach(SubscriptionListSort.allCases, id: \\.self) { item in\n                        Label(String(localized: item.label), icon: item.icon)\n                    }\n                }\n                .labelsHidden()\n                .pickerStyle(.inline)\n            }\n            if sort == .alphabetical {\n                Section(\"Row Size\") {\n                    Picker(\"Row Size\", icon: .settings.qualifiedLabel, selection: $instanceLocation) {\n                        Label(\"Large\", icon: .settings.postSizeLarge).tag(InstanceLocation.bottom)\n                        Label(\"Compact\", icon: .settings.postSizeCompact).tag(InstanceLocation.trailing)\n                    }\n                    .labelsHidden()\n                    .pickerStyle(.inline)\n                }\n            }\n            if UIDevice.isPad {\n                Toggle(\"Show Sidebar on App Launch\", icon: .settings.sidebar, isOn: $sidebarVisibleByDefault)\n            }\n        }\n        .animation(.easeOut(duration: 0.1), value: sort)\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Subscription List\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/SwipeActionEditorView.swift",
    "content": "//\n//  NewSwipeActionEditorView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-04.\n//\n\nimport Actions\nimport SwiftUI\n\nstruct SwipeActionEditorView: View {\n    @Binding var configuration: ActionSeedSwipeConfiguration\n    let onReset: () -> Void\n    let onApplyToAll: (() -> Void)?\n    let allActions: [ActionSeed]\n\n    @State var showingApplyToAllConfirmation: Bool = false\n\n    var body: some View {\n        Form {\n            ActionListView(\n                title: \"Left\",\n                actions: Binding(get: { configuration.leading }, set: { configuration.leading = $0 }),\n                allActions: allActions\n            )\n            ActionListView(\n                title: \"Right\",\n                actions: Binding(get: { configuration.trailing }, set: { configuration.trailing = $0 }),\n                allActions: allActions\n            )\n            Button(\"Reset\", action: onReset)\n            if let onApplyToAll {\n                Button(\"Apply to All\") { showingApplyToAllConfirmation = true }\n                    .confirmationDialog(\n                        \"Really apply this configuration to all other content types?\",\n                        isPresented: $showingApplyToAllConfirmation,\n                        titleVisibility: .visible\n                    ) {\n                        Button(\"Yes\", action: onApplyToAll)\n                    }\n            }\n        }\n        .environment(\\.editMode, .constant(.active))\n        .navigationTitle(\"Swipe Actions\")\n    }\n}\n\nextension SwipeActionEditorView {\n    init<Configuration: SwipeActionConfiguration>(\n        _ keyPath: ReferenceWritableKeyPath<SettingsValues, Configuration>,\n        onApplyToAll onApplyToAllConfiguration: ((Configuration) -> Void)? = nil\n    ) {\n        let onApplyToAll: (() -> Void)?\n        if let onApplyToAllConfiguration {\n            onApplyToAll = {\n                onApplyToAllConfiguration(Settings.get(keyPath))\n            }\n        } else {\n            onApplyToAll = nil\n        }\n\n        self.init(\n            configuration: .init(\n                get: {\n                    Settings.get(keyPath).swipes\n                }, set: { newValue in\n                    Settings.mutate(keyPath) { $0.swipes = newValue }\n                }\n            ),\n            onReset: {\n                var configuration = Settings.get(keyPath)\n                configuration.swipes = Configuration.defaultSwipes\n                Settings.set(keyPath, to: configuration)\n            },\n            onApplyToAll: onApplyToAll,\n            allActions: Configuration.availableActions.all\n        )\n    }\n}\n\nprivate struct ActionListView: View {\n    let title: LocalizedStringResource\n    @Binding var actions: [ActionSeed]\n    var allActions: [ActionSeed]\n    \n    var body: some View {\n        Section(title) {\n            ForEach(actions, id: \\.hashValue) { action in\n                HStack {\n                    Label(action.label.title, icon: action.label.icon)\n                        .symbolVariant(.fill)\n                        .gradientTint(action.label.color)\n                    Spacer()\n                }\n                .tag(action)\n            }\n            .onMove { old, new in\n                actions.move(fromOffsets: old, toOffset: new)\n            }\n            .onDelete { offsets in\n                actions.remove(atOffsets: offsets)\n            }\n            .labelStyle(.squircle)\n            addButtonView\n                .disabled(actions.count >= 3)\n        }\n    }\n    \n    @ViewBuilder\n    var addButtonView: some View {\n        Menu(\"Add\", icon: .general.add) {\n            ForEach(allActions, id: \\.self) { action in\n                Button(action.label.title, icon: action.label.icon) {\n                    actions.append(action)\n                }\n                .disabled(actions.contains(action))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/TabBarSettingsView.swift",
    "content": "//\n//  TabBarSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-05.\n//\n\nimport Icons\nimport SwiftUI\nimport Theming\n\nstruct TabBarSettingsView: View {\n    @Environment(AppState.self) var appState\n    \n    @Setting(\\.tab_profile_labelType) var profileTabLabel: ProfileTabLabel\n    @Setting(\\.tab_profile_showAvatar) var showUserAvatar: Bool\n    @Setting(\\.tab_gestures_longPressAction) var longPressAction: TabBarLongPressAction\n    @Setting(\\.tab_inbox_badgeIncludedTypes) var tabInboxBadgeIncludedTypes\n    \n    var account: any Account {\n        appState.firstAccount\n    }\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Tab Bar\",\n                description: \"Customize the appearance of the tab bar.\",\n                icon: .settings.tabBar\n            )\n            .gradientTint(.themedColorfulAccent(5))\n            Section(\"Profile Tab Label\") {\n                Picker(\"Profile Tab Label\", selection: $profileTabLabel) {\n                    profileTabLabelItem(\"Name\", value: account.nickname, icon: .lemmy.alphabeticalSort)\n                        .tag(ProfileTabLabel.nickname)\n                    profileTabLabelItem(\"Instance\", value: account.host, icon: .settings.qualifiedLabel)\n                        .tag(ProfileTabLabel.instance)\n                    profileTabLabelItem(\"Anonymous\", value: .init(localized: \"Profile\"), icon: .general.circle)\n                        .tag(ProfileTabLabel.anonymous)\n                }\n                .labelsHidden()\n                .pickerStyle(.inline)\n            }\n            Section {\n                Toggle(\"Show Avatar\", icon: .lemmy.person, isOn: $showUserAvatar)\n                    .symbolVariant(.circle)\n            }\n            \n            if !UIDevice.isIos26 {\n                Section {\n                    NavigationLink(\n                        \"Long Press Action\",\n                        value: .init(localized: longPressAction.label),\n                        fallbackValue: \"\",\n                        icon: .settings.longPress,\n                        destination: .settings(.longPressAction)\n                    )\n                }\n            }\n                \n            Section {\n                NavigationLink(\n                    \"Notification Badge\",\n                    value: tabInboxBadgeIncludedTypes.label(accountType: AccountsTracker.main.highestLevelAccountType),\n                    fallbackValue: .init(localized: \"Some\"),\n                    icon: .settings.unreadBadge,\n                    destination: .settings(.inboxBadge)\n                )\n            }\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Tab Bar\")\n    }\n    \n    @ViewBuilder\n    func profileTabLabelItem(_ title: LocalizedStringKey, value: String, icon: Icon) -> some View {\n        Label {\n            VStack(alignment: .leading) {\n                Text(title)\n                Text(value)\n                    .font(.footnote)\n                    .foregroundStyle(.themedSecondary)\n            }\n        } icon: {\n            Image(icon: icon)\n        }\n    }\n}\n\nenum ProfileTabLabel: String, Codable, CaseIterable {\n    case nickname, instance, anonymous\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/TappableLinksSettingsView.swift",
    "content": "//\n//  TappableLinksSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-28.\n//\n\nimport SwiftUI\n\nstruct TappableLinksSettingsView: View {\n    @Setting(\\.links_displayMode) var tappableLinksDisplayMode\n    \n    var body: some View {\n        Form {\n            Section {\n                Toggle(\n                    \"Tappable Links\",\n                    icon: .settings.tappableLinks,\n                    isOn: Binding(\n                        get: { tappableLinksDisplayMode != .disabled },\n                        set: { newValue in\n                            withAnimation(.easeOut(duration: 0.1)) {\n                                tappableLinksDisplayMode = newValue ? .large : .disabled\n                            }\n                        }\n                    )\n                )\n            }\n            if tappableLinksDisplayMode != .disabled {\n                Section(\"Show Full URL\") {\n                    Picker(\"Show Full URL\", icon: .markdown.inlineCode, selection: $tappableLinksDisplayMode) {\n                        Text(\"Automatic\").tag(TappableLinksDisplayMode.contextual)\n                        Text(\"Always\").tag(TappableLinksDisplayMode.large)\n                        Text(\"Never\").tag(TappableLinksDisplayMode.compact)\n                    }\n                    .pickerStyle(.inline)\n                    .labelsHidden()\n                } footer: {\n                    if tappableLinksDisplayMode != .disabled {\n                        Text(\"If set to \\\"Automatic\\\", the full URL will be hidden in compact comments.\")\n                    }\n                }\n            }\n        }\n        .navigationTitle(\"Tappable Links\")\n        .withConditionalLabelStyle()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ThemeSettingsView.swift",
    "content": "//\n//  ThemeSettingsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/05/2024.\n//\n\nimport SwiftUI\n\nstruct ThemeSettingsView: View {\n    @Setting(\\.appearance_interfaceStyle) var interfaceStyle\n    @Setting(\\.appearance_palette) var colorPalette\n    \n    // convenience\n    var supportedModes: UIUserInterfaceStyle { colorPalette.supportedModes }\n    \n    var body: some View {\n        Form {\n            Section {\n                // When a single-mode theme is selected, the picker will _display_ that mode as selected but not actually change the settings value\n                // so that it reverts the the actual settings value when a multi-mode theme is selected\n                Picker(\"Style\", selection: supportedModes == .unspecified ? $interfaceStyle : .constant(supportedModes)) {\n                    ForEach(UIUserInterfaceStyle.optionCases, id: \\.self) { style in\n                        interfaceStyleLabel(for: style)\n                    }\n                }\n                .labelsHidden()\n                .pickerStyle(.inline)\n            } footer: {\n                if supportedModes != .unspecified {\n                    Text(\"The \\(colorPalette.label) theme only supports \\(supportedModes.label.lowercased()) mode.\")\n                }\n            }\n            \n            Picker(\"Theme\", selection: $colorPalette) {\n                ForEach(PaletteOption.allCases, id: \\.rawValue) { item in\n                    ThemeLabel(palette: item)\n                        .tag(item)\n                }\n                .labelStyle(.titleAndIcon)\n            }\n            .labelsHidden()\n            .pickerStyle(.inline)\n        }\n        .withConditionalLabelStyle()\n        .navigationTitle(\"Theme\")\n    }\n    \n    @ViewBuilder\n    func interfaceStyleLabel(for style: UIUserInterfaceStyle) -> some View {\n        Label(style.label, icon: style.icon)\n            .foregroundStyle(\n                supportedModes == .unspecified || supportedModes == style\n                    ? .themedPrimary\n                    : .themedSecondary\n            )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/Tabs/Settings/ZoomSliderSettingsView.swift",
    "content": "//\n//  ZoomSliderSettingsView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-02-02.\n//\n\nimport SwiftUI\n\nenum AnimationPhase: CaseIterable {\n    case slideUp, slideDown, hide, show\n    \n    var circleOffset: CGFloat {\n        switch self {\n        case .slideUp: -50\n        case .slideDown: 50\n        case .hide: 50\n        case .show: 50\n        }\n    }\n    \n    var circleOpacity: CGFloat {\n        switch self {\n        case .slideUp: 1\n        case .slideDown: 1\n        case .hide: 0\n        case .show: 1\n        }\n    }\n    \n    var imageScale: CGFloat {\n        switch self {\n        case .slideUp: 2.0\n        case .slideDown: 0.8\n        case .hide: 0.8\n        case .show: 0.8\n        }\n    }\n    \n    var imageSize: CGFloat {\n        switch self {\n        case .slideUp: 400\n        case .slideDown: 150\n        case .hide: 150\n        case .show: 150\n        }\n    }\n}\n\nstruct ZoomSliderSettingsView: View {\n    @Setting(\\.a11y_zoomSliderLocation) var zoomSliderLocation\n    \n    var body: some View {\n        Form {\n            SettingsHeaderView(\n                title: \"Slide to Zoom\",\n                description: \"Zoom the image viewer with a slide gesture on the selected side.\"\n            ) {\n                ZoomSliderAnimation()\n            }\n            \n            Picker(\"Location\", selection: $zoomSliderLocation) {\n                ForEach(ZoomSliderLocation.allCases, id: \\.self) { location in\n                    Label(location.label.key, icon: location.icon)\n                        .tag(location)\n                }\n            }\n            .labelsHidden()\n            .pickerStyle(.inline)\n        }\n        .withConditionalLabelStyle()\n        .contentMargins(.top, 16)\n        .hiddenNavigationTitle(\"Slide to Zoom\")\n    }\n}\n\nstruct ZoomSliderAnimation: View {\n    var body: some View {\n        RoundedRectangle(cornerRadius: 10)\n            .fill(.black)\n            .frame(width: 72, height: 152)\n            .phaseAnimator(AnimationPhase.allCases) { content, phase in\n                content\n                    .overlay {\n                        Image(systemName: \"bird.fill\")\n                            .resizable()\n                            .scaledToFit()\n                            .scaleEffect(phase.imageScale)\n                            .foregroundStyle(.white.opacity(0.8))\n                    }\n                    .clipped()\n                    .overlay(alignment: .leading) {\n                        Circle()\n                            .frame(width: 10, height: 10)\n                            .foregroundStyle(.themedAccent)\n                            .opacity(phase.circleOpacity)\n                            .offset(y: phase.circleOffset)\n                            .padding(.leading, 4)\n                    }\n                    .clipShape(RoundedRectangle(cornerRadius: 10))\n            } animation: { phase in\n                switch phase {\n                case .hide: .easeOut(duration: 1.0)\n                case .show: .easeOut(duration: 0.1)\n                default: .easeInOut(duration: 0.75)\n                }\n            }\n            .overlay {\n                RoundedRectangle(cornerRadius: 10)\n                    .strokeBorder(.themedNeutralAccent, lineWidth: 2)\n            }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Root/TransitionView.swift",
    "content": "//\n//  AccountTransitionView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-10-02.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct TransitionView: View {\n    let account: any Account\n    @State var accountNameOpacity: CGFloat = .zero\n    \n    var body: some View {\n        let text = text()\n        let lines = lines(string: text)\n        VStack(alignment: .center, spacing: 24) {\n            Text(lines[0])\n            if lines.count == 2 {\n                Text(lines[1])\n                    .opacity(accountNameOpacity)\n            }\n        }\n        .accessibilityElement(children: .ignore)\n        .accessibilityLabel(text.replacingOccurrences(of: \"%@\", with: account.nickname))\n        .onAppear {\n            withAnimation(.easeIn(duration: 0.5)) {\n                accountNameOpacity = 1.0\n            }\n        }\n        .font(.largeTitle)\n        .bold()\n        .frame(maxWidth: .infinity, maxHeight: .infinity)\n        .padding(.horizontal, 50)\n    }\n    \n    func text() -> String {\n        let resource: LocalizedStringResource\n        if account is UserAccount {\n            resource = .init(\"Welcome %@\", comment: \"Example: \\\"Welcome John\\\"\")\n        } else {\n            resource = .init(\"Welcome to %@\", comment: \"Example: \\\"Welcome to lemmy.world\\\"\")\n        }\n        return .init(localized: resource)\n    }\n    \n    // Return type will either be of length 1 or 2\n    func lines(string: String) -> [String] {\n        if string.hasSuffix(\" %@\") {\n            return [String(string.dropLast(3)), account.nickname]\n        }\n        if string.hasPrefix(\"%@ \") {\n            return [account.nickname, String(string.dropFirst(3))]\n        }\n        \n        return [string.replacingOccurrences(of: \"%@\", with: account.nickname)]\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/AccountPickerMenu.swift",
    "content": "//\n//  AccountPickerMenu.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/07/2024.\n//\n\nimport SwiftUI\n\nstruct AccountPickerMenu<Content: View>: View {\n    var accountsTracker: AccountsTracker { .main }\n    \n    @Binding var account: UserAccount\n    let content: Content\n    \n    init(account: Binding<UserAccount>, @ViewBuilder content: () -> Content) {\n        self._account = account\n        self.content = content()\n    }\n    \n    var body: some View {\n        Menu {\n            Picker(\"Switch Account\", selection: $account) {\n                ForEach(accountsTracker.userAccounts, id: \\.actorId) { account in\n                    Button {} label: {\n                        Label(account)\n                        Text(verbatim: \"@\\(account.host)\")\n                    }\n                    .tag(account)\n                }\n            }\n            .pickerStyle(.inline)\n        } label: {\n            // This `Button` wrapper is necessary, otherwise the `Picker` won't work.\n            Button(action: {}, label: {\n                content\n            })\n        }\n        .buttonStyle(.plain)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Accounts/AccountListView+Logic.swift",
    "content": "//\n//  AccountListView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 22/12/2023.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension AccountListView {\n    var accounts: [any Account] {\n        let accountSort = accountsTracker.userAccounts.count == 2 ? .custom : accountSort\n        switch accountSort {\n        case .custom:\n            return accountsTracker.userAccounts\n        case .name:\n            return accountsTracker.userAccounts.sorted { $0.nicknameSortKey < $1.nicknameSortKey }\n        case .instance:\n            return accountsTracker.userAccounts.sorted { $0.instanceSortKey < $1.instanceSortKey }\n        case .mostRecent:\n            return accountsTracker.userAccounts.sorted { left, right in\n                if appState.firstSession.actorId == left.actorId {\n                    return true\n                } else if appState.firstSession.actorId == right.actorId {\n                    return false\n                }\n                return left.activityState.lastUsed ?? .distantPast > right.activityState.lastUsed ?? .distantPast\n            }\n        }\n    }\n    \n    func getNameCategory(account: any Account) -> String {\n        guard let first = account.nickname.first else { return \"Unknown\" }\n        if first.isLetter {\n            return String(first.lowercased())\n        }\n        return \"*\"\n    }\n    \n    var accountGroups: [AccountGroup] {\n        switch accountSort {\n        case .custom:\n            return [.init(header: \"Custom\", accounts: accountsTracker.userAccounts)]\n        case .name:\n            return Dictionary(\n                grouping: accountsTracker.userAccounts,\n                by: { getNameCategory(account: $0) }\n            ).map { AccountGroup(header: $0, accounts: $1.sorted { $0.nicknameSortKey < $1.nicknameSortKey }) }\n                .sorted { $0.header < $1.header }\n        case .instance:\n            let dict = Dictionary(\n                grouping: accountsTracker.userAccounts,\n                by: \\.host\n            )\n            let uniqueInstances = dict.filter { $1.count == 1 }.values.map { $0.first! }\n            var array = dict\n                .filter { $1.count > 1 }\n                .map { AccountGroup(header: $0, accounts: $1.sorted { $0.nicknameSortKey < $1.nicknameSortKey }) }\n                .sorted { $0.header < $1.header }\n            array.append(\n                AccountGroup(\n                    header: \"Other\",\n                    accounts: uniqueInstances.sorted { $0.instanceSortKey < $1.instanceSortKey }\n                )\n            )\n            return array\n        case .mostRecent:\n            var today = [any Account]()\n            var last30Days = [any Account]()\n            var older = [any Account]()\n            for account in accountsTracker.userAccounts {\n                if account.actorId == appState.firstSession.actorId {\n                    continue\n                }\n                \n                var dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: .now)\n                dateComponents.hour = 0\n                dateComponents.minute = 0\n                dateComponents.second = 0\n                let todayDate = Calendar.current.date(from: dateComponents) ?? .distantFuture\n                \n                if let date = account.activityState.lastUsed {\n                    if date > todayDate {\n                        today.append(account)\n                    } else if date.timeIntervalSinceNow <= 60 * 60 * 24 * 7 {\n                        last30Days.append(account)\n                    } else {\n                        older.append(account)\n                    }\n                } else {\n                    older.append(account)\n                }\n            }\n            var groups = [AccountGroup]()\n            \n            today.sort { $0.activityState.lastUsed ?? .distantPast > $1.activityState.lastUsed ?? .distantPast }\n            today.prepend(appState.firstSession.account)\n            \n            if !today.isEmpty {\n                groups.append(\n                    AccountGroup(\n                        header: \"Today\",\n                        accounts: today\n                    )\n                )\n            }\n            if !last30Days.isEmpty {\n                groups.append(\n                    AccountGroup(\n                        header: \"Last \\(30) days\",\n                        accounts: last30Days.sorted {\n                            $0.activityState.lastUsed ?? .distantPast > $1.activityState.lastUsed ?? .distantPast\n                        }\n                    )\n                )\n            }\n            if !older.isEmpty {\n                groups.append(\n                    AccountGroup(\n                        header: \"Older\",\n                        accounts: older.sorted {\n                            $0.activityState.lastUsed ?? .distantPast > $1.activityState.lastUsed ?? .distantPast\n                        }\n                    )\n                )\n            }\n            return groups\n        }\n    }\n    \n    func reorderAccount(fromOffsets: IndexSet, toOffset: Int) {\n        accountsTracker.userAccounts.move(fromOffsets: fromOffsets, toOffset: toOffset)\n        accountsTracker.saveAccounts(ofType: .user)\n    }\n    \n    func listRowComplications(withInstance: Bool) -> Set<AccountListRowBody.Complication> {\n        var complications: Set<AccountListRowBody.Complication> = [.unreadCount, .isActive]\n        if withInstance {\n            complications.insert(.instance)\n        }\n        switch preferredListRowComplication {\n        case .lastUsed:\n            complications.insert(.lastUsed)\n        case .responseTime:\n            complications.insert(.responseTime)\n        }\n        return complications\n    }\n    \n    func fetchUnreadCounts() {\n        for account in accountsTracker.allAccounts {\n            Task {\n                let startTime = Date.now\n                let unreadCount = try? await account.api.getUnreadCount(alwaysMakeCalls: true)\n                self.unreadCountResponses[account.actorId] = .init(\n                    unreadCount: unreadCount,\n                    responseTime: Date.now.timeIntervalSince(startTime)\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Accounts/AccountListView.swift",
    "content": "//\n//  AccountListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 22/12/2023.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Icons\n\n/// This view is a component used as a child of ``QuickSwitcherView`` and ``AccountListSettingsView``.\nstruct AccountListView: View {\n    @Setting(\\.accounts_sort) var accountSort\n    @Setting(\\.accounts_grouped) var groupAccountSort\n    @Setting(\\.accounts_preferredListRowComplication) var preferredListRowComplication\n\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.dismiss) var dismiss\n    \n    var accountsTracker: AccountsTracker { .main }\n    \n    @State var isSwitching: Bool = false\n    \n    @State private var isShowingAddAccountDialogue: Bool = false\n    \n    @State var isFetchingUnreadCounts: Bool = false\n    @State var unreadCountResponses: [ActorIdentifier: UnreadCountResponse] = [:]\n    \n    struct UnreadCountResponse {\n        let unreadCount: UnreadCount?\n        let responseTime: TimeInterval\n    }\n    \n    struct AccountGroup {\n        let header: String\n        let accounts: [any Account]\n        \n        init(header: LocalizedStringResource, accounts: [any Account]) {\n            self.header = .init(localized: header)\n            self.accounts = accounts\n        }\n        \n        @_disfavoredOverload\n        init(header: some StringProtocol, accounts: [any Account]) {\n            self.header = String(header)\n            self.accounts = accounts\n        }\n    }\n    \n    let isQuickSwitcher: Bool\n    \n    init(isQuickSwitcher: Bool = false) {\n        self.isQuickSwitcher = isQuickSwitcher\n    }\n    \n    var shouldAllowReordering: Bool {\n        (accountSort == .custom || accountsTracker.userAccounts.count == 2) && !isQuickSwitcher\n    }\n    \n    var body: some View {\n        Group {\n            if !isSwitching {\n                if accountsTracker.userAccounts.count > 3, groupAccountSort {\n                    groupedUserAccountList\n                } else if accounts.isEmpty {\n                    Text(\"You don't have any accounts.\")\n                        .foregroundStyle(.themedSecondary)\n                } else {\n                    Section {\n                        ForEach(accounts, id: \\.actorId) { account in\n                            accountListRow(account: account)\n                        }\n                        .onMove(perform: shouldAllowReordering ? reorderAccount : nil)\n                    } header: {\n                        topHeader()\n                    }\n                }\n                if let account = (appState.firstSession as? GuestSession)?.account, !account.isSaved {\n                    Section {\n                        AccountListRow(account: account, isSwitching: $isSwitching)\n                    }\n                }\n                Section {\n                    ForEach(accountsTracker.guestAccounts, id: \\.actorId) { account in\n                        accountListRow(account: account)\n                    }\n                }\n                addAccountButton\n            }\n        }\n        .onAppear {\n            if !isFetchingUnreadCounts {\n                isFetchingUnreadCounts = true\n                fetchUnreadCounts()\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var groupedUserAccountList: some View {\n        ForEach(Array(accountGroups.enumerated()), id: \\.offset) { offset, group in\n            Section {\n                ForEach(group.accounts, id: \\.actorId) { account in\n                    accountListRow(\n                        account: account,\n                        withInstanceComplication: accountSort != .instance || group.header == \"Other\"\n                    )\n                }\n            } header: {\n                if offset == 0 {\n                    topHeader(text: group.header)\n                } else {\n                    Text(group.header)\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var addAccountButton: some View {\n        Section {\n            Button { isShowingAddAccountDialogue = true } label: {\n                Label(\"Add Account\", icon: .general.add)\n                    .labelStyle(.titleAndIcon)\n            }\n            .confirmationDialog(\"Choose Account Type\", isPresented: $isShowingAddAccountDialogue) {\n                Button(\"Log In\") {\n                    navigation.openSheet(.logIn())\n                }\n                Button(\"Sign Up\") {\n                    navigation.openSheet(.signUp())\n                }\n                Button(\"Add Guest\") {\n                    navigation.openSheet(.instancePicker(callback: { instance in\n                        if let url = URL(string: \"https://\\(instance.host)\") {\n                            if let guest = try? GuestAccount.getGuestAccount(url: url) {\n                                if !guest.isSaved {\n                                    AccountsTracker.main.addAccount(account: guest)\n                                }\n                                AppState.main.changeAccount(to: guest)\n                                if navigation.isInsideSheet {\n                                    dismiss()\n                                }\n                            }\n                        }\n                    }))\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func accountListRow(account: any Account, withInstanceComplication: Bool = true) -> some View {\n        let responseData = unreadCountResponses[account.actorId]\n        AccountListRow(\n            account: account,\n            unreadCount: responseData?.unreadCount?.badgeLabel,\n            responseTime: responseData?.responseTime,\n            complications: listRowComplications(withInstance: withInstanceComplication),\n            isSwitching: $isSwitching\n        )\n    }\n    \n    @ViewBuilder\n    func topHeader(text: String? = nil) -> some View {\n        HStack {\n            if let text {\n                Text(text)\n            }\n            if !isQuickSwitcher, accountsTracker.userAccounts.count > 2 {\n                Spacer()\n                sortModeMenu()\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func sortModeMenu() -> some View {\n        Menu {\n            Picker(\"Sort\", selection: $accountSort) {\n                ForEach(AccountSortMode.allCases, id: \\.self) { sortMode in\n                    Label(String(localized: sortMode.label), systemImage: sortMode.systemImage).tag(sortMode)\n                }\n            }\n            .onChange(of: accountSort) {\n                if accountSort == .custom {\n                    groupAccountSort = false\n                }\n            }\n            if accountsTracker.userAccounts.count > 3 {\n                Divider()\n                Toggle(\"Grouped\", icon: .lemmy.groupAccountSort, isOn: $groupAccountSort)\n                .disabled(accountSort == .custom)\n            }\n        } label: {\n            HStack(alignment: .center, spacing: 2) {\n                Text(\"Sort by: \\(accountSort.label)\")\n                    .font(.caption)\n                    .textCase(nil)\n                Image(systemName: \"chevron.down\")\n                    .imageScale(.small)\n            }\n            .fontWeight(.semibold)\n            .foregroundStyle(.themedAccent)\n        }\n        .textCase(nil)\n        .labelStyle(.titleAndIcon) // Override `.conditional` label style from parent view\n    }\n}\n\nenum PreferredAccountListRowComplication: String, Codable {\n    case lastUsed, responseTime\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Accounts/QuickSwitcherView.swift",
    "content": "//\n//  QuickSwitcherView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-02-21.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct QuickSwitcherView: View {\n    @Environment(\\.scenePhase) var scenePhase\n    @Environment(NavigationLayer.self) var navigation\n    \n    var body: some View {\n        Form {\n            AccountListView(isQuickSwitcher: true)\n        }\n        .onChange(of: scenePhase) {\n            // when app moves into background, hide the account switcher. This prevents the app from reopening with the switcher presented.\n            if scenePhase != .active, navigation.isTopSheet {\n                navigation.dismissSheet()\n            }\n        }\n        .presentationBackground(.themedGroupedBackground)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Avatar/AvatarBannerView.swift",
    "content": "//\n//  AvatarBannerView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 09/05/2024.\n//\n\nimport MlemMiddleware\nimport NukeUI\nimport Rest\nimport SwiftUI\n\nstruct AvatarBannerView: View {\n    @Setting(\\.media_animatedAvatars) var animatedAvatars\n    \n    var model: (any ProfileProviding)?\n    var fallback: MediaView.Fallback\n    var showEmptyBanner: Bool = false\n    var showBanner: Bool = true\n    var showAvatar: Bool = true\n\n    init<T: ProfileProviding>(_ model: T?, showEmptyBanner: Bool = false) {\n        self.model = model\n        self.fallback = T.avatarFallback\n        self.showEmptyBanner = showEmptyBanner\n    }\n    \n    init(_ model: any ProfileProviding, showEmptyBanner: Bool = false) {\n        self.model = model\n        self.fallback = Swift.type(of: model).avatarFallback\n        self.showEmptyBanner = showEmptyBanner\n    }\n    \n    init(_ model: (any ProfileProviding)?, fallback: MediaView.Fallback, showEmptyBanner: Bool = false) {\n        self.model = model\n        self.fallback = fallback\n        self.showEmptyBanner = showEmptyBanner\n    }\n    \n    static let bannerHeight: CGFloat = 170\n    static let avatarOverdraw: CGFloat = 40\n    static let avatarSize: CGFloat = 108\n    static let avatarPadding: CGFloat = Constants.main.standardSpacing\n    \n    var body: some View {\n        Group {\n            if model?.banner != nil || showEmptyBanner, showBanner {\n                ZStack(alignment: .bottom) {\n                    VStack {\n                        LazyImage(request: imageRequest) { state in\n                            VStack {\n                                if let image = state.image {\n                                    image\n                                        .resizable()\n                                        .aspectRatio(contentMode: .fill)\n                                        .clipped()\n                                } else {\n                                    Color(uiColor: .secondarySystemFill)\n                                }\n                            }\n                            .frame(minWidth: 0, maxWidth: .infinity)\n                            .frame(height: AvatarBannerView.bannerHeight)\n                            .clipped()\n                            .clipShape(RoundedRectangle(cornerRadius: Constants.main.mediumItemCornerRadius))\n                            .mask {\n                                ZStack(alignment: .bottom) {\n                                    Color.black\n                                    if showAvatar {\n                                        Circle()\n                                            .frame(\n                                                width: AvatarBannerView.avatarSize + AvatarBannerView.avatarPadding * 2,\n                                                height: AvatarBannerView.avatarSize + AvatarBannerView.avatarPadding * 2\n                                            )\n                                            .offset(y: AvatarBannerView.avatarOverdraw + AvatarBannerView.avatarPadding)\n                                            .blendMode(.destinationOut)\n                                    }\n                                }\n                                .compositingGroup()\n                            }\n                        }\n                        Spacer()\n                    }\n                    .overlay {\n                        if showAvatar {\n                            avatarView\n                                .frame(maxHeight: .infinity, alignment: .bottom)\n                        }\n                    }\n                }\n                .frame(height: AvatarBannerView.bannerHeight + (showAvatar ? AvatarBannerView.avatarOverdraw : 0))\n            } else {\n                if showAvatar {\n                    avatarView\n                        .padding(.top)\n                }\n            }\n        }\n    }\n    \n    var imageRequest: ImageRequest? {\n        if let url = model?.banner {\n            .init(urlRequest: mlemUrlRequest(url: url))\n        } else {\n            nil\n        }\n    }\n    \n    var avatarView: some View {\n        CircleCroppedImageView(\n            url: model?.avatar,\n            frame: AvatarBannerView.avatarSize,\n            fallback: fallback,\n            enableAnimation: animatedAvatars != .never\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Avatar/AvatarStackView.swift",
    "content": "//\n//  AvatarStackView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/05/2024.\n//\n\nimport SwiftUI\n\nstruct AvatarStackView: View {\n    let urls: [URL?]\n    let fallback: MediaView.Fallback\n    \n    let height: CGFloat\n    let spacing: CGFloat\n    let outlineWidth: CGFloat\n    \n    var showPlusIcon: Bool = false\n    \n    var body: some View {\n        HStack(spacing: 0) {\n            Spacer().aspectRatio(1 / 2, contentMode: .fit)\n            HStack(spacing: spacing) {\n                ForEach(showPlusIcon ? urls : urls.dropLast(), id: \\.self) { url in\n                    avatarView(url: url)\n                        .frame(maxWidth: 0)\n                        .padding(outlineWidth)\n                        .mask {\n                            Rectangle()\n                                .subtracting(.circle.offset(x: spacing))\n                                .aspectRatio(contentMode: .fill)\n                        }\n                }\n                if showPlusIcon {\n                    plusIconView\n                        .frame(maxWidth: 0)\n                        .padding(outlineWidth)\n                } else {\n                    avatarView(url: urls.last ?? nil)\n                        .frame(maxWidth: 0)\n                        .padding(outlineWidth)\n                }\n            }\n            Spacer().aspectRatio(1 / 2, contentMode: .fit)\n        }\n    }\n    \n    @ViewBuilder\n    var plusIconView: some View {\n        Image(systemName: \"plus.circle.fill\")\n            .resizable()\n            .aspectRatio(contentMode: .fill)\n            .symbolRenderingMode(.hierarchical)\n            .foregroundStyle(.secondary, Color(uiColor: .tertiaryLabel))\n    }\n    \n    @ViewBuilder\n    func avatarView(url: URL?) -> some View {\n        CircleCroppedImageView(\n            url: url,\n            frame: height,\n            fallback: fallback\n        )\n        .aspectRatio(contentMode: .fill)\n    }\n}\n\n#Preview {\n    AvatarStackView(\n        urls: .init(repeating: nil, count: 3),\n        fallback: .personAvatar,\n        height: 64,\n        spacing: 48,\n        outlineWidth: 1\n    )\n    .frame(height: 64)\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Avatar/AvatarView.swift",
    "content": ""
  },
  {
    "path": "Mlem/App/Views/Shared/Avatar/ProfileHeaderView.swift",
    "content": "//\n//  ProfileHeaderView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/05/2024.\n//\n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ProfileHeaderView: View {\n    @Environment(AppState.self) var appState\n    \n    var profilable: (any ProfileProviding)?\n    var fallback: MediaView.Fallback\n    var blockedOverride: Bool?\n    \n    init<T: ProfileProviding>(_ profilable: T?, blockedOverride: Bool? = nil) {\n        self.profilable = profilable\n        self.fallback = T.avatarFallback\n        self.blockedOverride = blockedOverride\n    }\n    \n    init(_ profilable: any ProfileProviding, blockedOverride: Bool? = nil) {\n        self.profilable = profilable\n        self.fallback = Swift.type(of: profilable).avatarFallback\n        self.blockedOverride = blockedOverride\n    }\n    \n    init(_ profilable: (any ProfileProviding)?, fallback: MediaView.Fallback, blockedOverride: Bool? = nil) {\n        self.profilable = profilable\n        self.fallback = fallback\n        self.blockedOverride = blockedOverride\n    }\n    \n    var body: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            AvatarBannerView(profilable, fallback: fallback)\n            Button {\n                (profilable as? any CommunityOrPerson)?.copyFullNameWithPrefix()\n            } label: {\n                VStack(spacing: Constants.main.halfSpacing) {\n                    HStack {\n                        Text(profilable?.displayName ?? \"\")\n                            .foregroundStyle(.themedPrimary)\n                        if blockedOverride ?? profilable?.blocked.realizedValue ?? false {\n                            Image(icon: .general.hide)\n                                .foregroundStyle(.themedSecondary)\n                        }\n                    }\n                    .font(.title)\n                    .fontWeight(.semibold)\n                    .lineLimit(1)\n                    .minimumScaleFactor(0.01)\n                    Text(subtitle)\n                        .font(.caption)\n                        .foregroundStyle(.themedSecondary)\n                }\n            }\n            .buttonStyle(.plain)\n        }\n        .frame(maxWidth: .infinity)\n    }\n    \n    var subtitle: String {\n        if let instance = profilable as? Instance {\n            return \"\\(instance.host) • \\(instance.software.value?.label ?? \"\")\"\n        }\n        return (profilable as? any CommunityOrPerson)?.fullNameWithPrefix ?? profilable?.host ?? \"\"\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Avatar/SimpleAvatarView.swift",
    "content": "//\n//  SimpleAvatarView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/07/2024.\n//\n\nimport Nuke\nimport SwiftUI\n\nstruct SimpleAvatarView: View {\n    @Environment(Palette.self) var palette\n    \n    @State private var uiImage: UIImage\n    @State private var loading: Bool\n    \n    let url: URL?\n    let type: AvatarType\n    \n    init(\n        url: URL?,\n        type: AvatarType\n    ) {\n        self.url = url\n        self.type = type\n    \n        self._uiImage = .init(wrappedValue: .init())\n        self._loading = .init(wrappedValue: url != nil)\n    }\n    \n    var defaultImage: UIImage {\n        .init(systemName: Icons.user)!\n            .applyingSymbolConfiguration(.init(\n                font: .systemFont(ofSize: 17),\n                scale: .large\n            ))!\n            .withTintColor(.secondaryLabel, renderingMode: .alwaysOriginal)\n    }\n    \n    var body: some View {\n        Group {\n            if url == nil {\n                Image(uiImage: defaultImage)\n            } else {\n                Image(uiImage: uiImage)\n                    .task(loadImage)\n            }\n        }\n    }\n    \n    @Sendable\n    func loadImage() async {\n        guard let url else { return }\n        \n        do {\n            let imageTask = ImagePipeline.shared.imageTask(with: url)\n            \n            let image = try await imageTask.image\n            uiImage = image.circleMasked ?? image\n            loading = false\n        } catch {\n            print(error)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Bubble Picker/BubblePickerView.swift",
    "content": "//\n//  BubblePickerView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 18/09/2023.\n//\n\nimport Dependencies\nimport Haptics\nimport SwiftUI\n\nenum DividerPlacement {\n    case top, bottom\n}\n\nstruct BubblePickerItemFrame: Hashable {\n    let width: CGFloat\n    let offset: CGFloat\n    \n    static var zero: Self {\n        .init(width: 0, offset: 0)\n    }\n}\n\nstruct BubblePicker<Value: Identifiable & Equatable & Hashable>: View {\n    @Environment(HapticManager.self) var hapticManager\n    @Binding var selected: Value\n    \n    // currentTabIndex is used to drive the capsule animation; it is tracked separately from selected so that the capsule animations can be triggered independently of any animation (or lack thereof) that is desired on selected\n    @State var currentTabIndex: Int\n    @State var selectedTabFrame: BubblePickerItemFrame?\n    \n    let tabs: [Value]\n    let dividers: Set<DividerPlacement>\n    let label: (Value) -> LocalizedStringResource\n    let value: (Value) -> Int?\n    let spaceName: String = UUID().uuidString\n    \n    let animation: Animation = .interactiveSpring(response: 0.2, dampingFraction: 0.8)\n\n    init(\n        _ tabs: [Value],\n        selected: Binding<Value>,\n        withDividers: Set<DividerPlacement> = .init(),\n        label: @escaping (Value) -> LocalizedStringResource,\n        value: @escaping (Value) -> Int? = { _ in nil }\n    ) {\n        let initialIndex = tabs.firstIndex(of: selected.wrappedValue)\n        \n        self._selected = selected\n        self._currentTabIndex = .init(wrappedValue: initialIndex ?? 0)\n        self.tabs = tabs\n        self.dividers = withDividers\n        self.label = label\n        self.value = value\n        \n        // gracefully handle cases where selected tab is not found\n        if initialIndex == nil {\n            Task { @MainActor in\n                selected.wrappedValue = tabs[0]\n            }\n        }\n    }\n    \n    var body: some View {\n        VStack(spacing: 0) {\n            if dividers.contains(.top) {\n                Divider()\n            }\n            \n            ScrollViewReader { scrollProxy in\n                ScrollView(.horizontal) {\n                    buttonStack(scrollProxy: scrollProxy, isSelectionIndicator: false)\n                        .overlay {\n                            buttonStack(isSelectionIndicator: true)\n                                .background(.themedAccent)\n                                .allowsHitTesting(false)\n                                .mask(alignment: .leading) {\n                                    if let selectedTabFrame {\n                                        Capsule()\n                                            .offset(x: selectedTabFrame.offset + Constants.main.standardSpacing)\n                                            .frame(width: max(selectedTabFrame.width - Constants.main.doubleSpacing, 0), height: 30)\n                                            .animation(animation, value: selectedTabFrame)\n                                    } else {\n                                        Color.clear\n                                    }\n                                }\n                        }\n                        .coordinateSpace(name: spaceName)\n                }\n                .scrollIndicators(.hidden)\n                .onChange(of: selected) {\n                    let newIndex = tabs.firstIndex(of: selected) ?? 0\n                    currentTabIndex = newIndex\n                    withAnimation(animation) {\n                        scrollProxy.scrollTo(newIndex)\n                    }\n                }\n                .id(tabs.hashValue)\n            }\n            \n            if dividers.contains(.bottom) {\n                Divider()\n            }\n        }\n    }\n    \n    /// Builds the HStack containing the actual buttons\n    /// - Parameter scrollProxy: scrollProxy to handle scrolling horizontally to the selected view. If present, the stack will create buttons and apply a ChildSizeReader to them to populate the size information for the masking; otherwise the stack will use inert labels.\n    @ViewBuilder\n    func buttonStack(\n        scrollProxy: ScrollViewProxy? = nil,\n        isSelectionIndicator: Bool\n    ) -> some View {\n        // Use negative spacing as well as padding the HStack's children so that scrollTo leaves extra space around each tab\n        HStack(spacing: -Constants.main.doubleSpacing) {\n            ForEach(Array(tabs.enumerated()), id: \\.offset) { index, tab in\n                if let scrollProxy {\n                    ChildSizeReader(\n                        size: tab == selected ? Binding(\n                            get: { selectedTabFrame ?? .zero },\n                            set: { selectedTabFrame = $0 }\n                        ) : nil,\n                        spaceName: spaceName\n                    ) {\n                        bubbleButton(\n                            index: index,\n                            tab: tab,\n                            scrollProxy: scrollProxy,\n                            isSelectionIndicator: isSelectionIndicator\n                        )\n                    }\n                    .id(\"\\(isSelectionIndicator)\\(value(tab) ?? -1)\")\n                } else {\n                    bubbleButtonLabel(tab: tab, isSelectionIndicator: isSelectionIndicator)\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func bubbleButton(\n        index: Int,\n        tab: Value,\n        scrollProxy: ScrollViewProxy,\n        isSelectionIndicator: Bool\n    ) -> some View {\n        Button {\n            selected = tab\n            hapticManager.play(haptic: .gentleInfo, tier: .low)\n        } label: {\n            bubbleButtonLabel(tab: tab, isSelectionIndicator: isSelectionIndicator)\n        }\n        .buttonStyle(.empty)\n        .id(index)\n    }\n    \n    @ViewBuilder\n    func bubbleButtonLabel(\n        tab: Value,\n        isSelectionIndicator: Bool\n    ) -> some View {\n        AnyView(HStack(spacing: 8) {\n            let value = value(tab)\n            Text(label(tab))\n                .font(.subheadline)\n                .fontWeight(.semibold)\n                .foregroundStyle(isSelectionIndicator ? .themedContrastingLabel : .themedPrimary)\n            if let value {\n                Text(value.abbreviated)\n                    .monospacedDigit()\n                    .font(.subheadline)\n                    .fontWeight(.medium)\n                    .foregroundStyle(isSelectionIndicator ? .themedContrastingLabel : .themedSecondary)\n                    .opacity(isSelectionIndicator ? 0.8 : 1)\n            }\n        })\n        .padding(.horizontal, 22)\n        .frame(minHeight: 50)\n        .contentShape(.rect)\n    }\n}\n\n// #Preview {\n//    @State var selected: InstanceViewTab = .administration\n//    return BubblePicker(\n//        InstanceViewTab.allCases,\n//        selected: $selected,\n//        label: { $0.label },\n//        value: { item in\n//            switch item {\n//            case .about:\n//                0\n//            case .administration:\n//                5\n//            case .details:\n//                9_950_000\n//            case .uptime:\n//                10_000_000\n//            default:\n//                nil\n//            }\n//        }\n//    )\n// }\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Bubble Picker/ChildSizeReader.swift",
    "content": "//\n//  ChildSizeReader.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-22.\n//\n// adapted from https://stackoverflow.com/questions/56573373/swiftui-get-size-of-child\n\nimport Foundation\nimport SwiftUI\n\nstruct ChildSizeReader<Content: View>: View {\n    init(\n        size: Binding<BubblePickerItemFrame>?,\n        spaceName: String,\n        @ViewBuilder content: @escaping () -> Content\n    ) {\n        self.selectedSize = size\n        self.spaceName = spaceName\n        self.content = content\n    }\n    \n    var selectedSize: Binding<BubblePickerItemFrame>?\n    \n    @State var size: BubblePickerItemFrame = .zero\n    \n    let spaceName: String\n    let content: () -> Content\n    \n    var body: some View {\n        ZStack {\n            content()\n                .background(\n                    GeometryReader { proxy in\n                        Color.clear\n                            .preference(key: SizePreferenceKey.self, value: .init(\n                                width: proxy.size.width,\n                                offset: proxy.frame(in: .named(spaceName)).minX\n                            ))\n                    }\n                )\n        }\n        .onPreferenceChange(SizePreferenceKey.self) {\n            if size == .zero {\n                size = $0\n            }\n        }\n        .onChange(of: onChangeHash, initial: true) {\n            selectedSize?.wrappedValue = size\n        }\n    }\n    \n    var onChangeHash: Int {\n        var hasher = Hasher()\n        hasher.combine(selectedSize == nil)\n        hasher.combine(size)\n        return hasher.finalize()\n    }\n}\n\nprivate struct SizePreferenceKey: PreferenceKey {\n    typealias Value = BubblePickerItemFrame\n    static var defaultValue: Value = .init(width: .zero, offset: .zero)\n\n    static func reduce(value _: inout Value, nextValue: () -> Value) {\n        _ = nextValue()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/BypassProxyWarningSheet.swift",
    "content": "//\n//  BypassProxyWarningSheet.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-09-13.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct BypassProxyWarningSheet: View {\n    @Setting(\\.privacy_autoBypassImageProxy) var autoBypassImageProxy\n    \n    @Environment(\\.dismiss) var dismiss\n    \n    let callback: () -> Void\n    \n    var body: some View {\n        VStack(spacing: Constants.main.doubleSpacing) {\n            WarningView(\n                icon: .lemmy.imageProxy,\n                text: \"Bypass Image Proxy?\",\n                inList: false,\n                overrideColor: .themedCaution\n            )\n            \n            // swiftlint:disable:next line_length\n            Text(\"Some instances proxy images to protect your privacy. In certain cases, this causes image loading to fail. You can bypass the image proxy and load directly, but this will expose your IP address to the image host.\")\n            \n            Button {\n                callback()\n                dismiss()\n            } label: {\n                Text(\"Load This Image Directly\")\n                    .padding(.vertical, Constants.main.halfSpacing)\n                    .frame(maxWidth: .infinity)\n            }\n            .buttonStyle(.bordered)\n            \n            VStack(spacing: Constants.main.halfSpacing) {\n                Button {\n                    autoBypassImageProxy = true\n                    callback()\n                    dismiss()\n                } label: {\n                    Text(\"Always Allow Direct Loading\")\n                        .padding(.vertical, Constants.main.halfSpacing)\n                        .frame(maxWidth: .infinity)\n                }\n                .buttonStyle(.bordered)\n                \n                Text(\"Mlem will always try to load from the proxy first.\")\n                    .font(.footnote)\n                    .foregroundStyle(.secondary)\n            }\n            \n            Button {\n                dismiss()\n            } label: {\n                Text(\"Cancel\")\n                    .padding(.vertical, Constants.main.halfSpacing)\n                    .frame(maxWidth: .infinity)\n            }\n            .buttonStyle(.borderedProminent)\n        }\n        .padding(Constants.main.doubleSpacing)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/CommentBodyView.swift",
    "content": "//\n//  CommentBodyView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 09/08/2024.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CommentBodyView: View {\n    @Environment(\\.exposeRemovedContent) var exposeRemovedContent\n    \n    @Setting(\\.comment_compact) var compactComments\n    \n    let comment: Comment\n    \n    var body: some View {\n        Group {\n            if comment.deleted {\n                missingContentMessage(\"Comment was deleted\")\n            } else if comment.removed {\n                if exposeRemovedContent {\n                    MarkdownWithLinkList(comment.content, configuration: .removedContent, showLinkCaptions: !compactComments)\n                } else {\n                    missingContentMessage(\"Comment was removed\")\n                }\n            } else {\n                MarkdownWithLinkList(comment.content, showLinkCaptions: !compactComments)\n            }\n        }\n        .fixedSize(horizontal: false, vertical: true)\n    }\n    \n    @ViewBuilder\n    func missingContentMessage(_ label: LocalizedStringResource) -> some View {\n        Text(label)\n            .italic()\n            .foregroundStyle(.themedSecondary)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/CommentView.swift",
    "content": "//\n//  CommentView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/06/2024.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CommentView<EmbeddedContent: View>: View {\n    @Environment(AppState.self) var appState\n    @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker?\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.communityContext) var communityContext: Community?\n    @Environment(\\.reportContext) private var reportContext: Report?\n    @Environment(\\.palette) private var palette\n    \n    @Setting(\\.comment_compact) var compactComments\n    @Setting(\\.comment_showDownvotesCompact) var showDownvotesCompact\n    @Setting(\\.menus_modActionGrouping) var moderatorActionGrouping\n    @Setting(\\.interactionBar_comment) var commentInteractionBar\n    @Setting(\\.interactionBar_commentReport) var commentReportInteractionBar\n    @Setting(\\.interactionBar_alternateReportLayout) var alternateInteractionBarLayoutForReports\n    \n    private let indent: CGFloat = 10\n    \n    let comment: Comment\n    \n    /// If the `CommentView` is rendered in an `ExpandedPostView`, this object can be used to access collapsed state etc.\n    let treeNode: CommentTreeNode?\n    \n    let embeddedContent: EmbeddedContent\n    let inFeed: Bool\n    let highlight: Bool\n    let depthOffset: Int\n    \n    var compactReadouts: [CommentBarConfiguration.ReadoutType] {\n        var readouts: [CommentBarConfiguration.ReadoutType] = [.created]\n        readouts.append(contentsOf: showDownvotesCompact ? [.upvote, .downvote, .comment] : [.score, .comment])\n        readouts.appendIfPresent(comment.saved.value ?? false ? .saved : nil)\n        return readouts\n    }\n    \n    init(\n        comment: Comment,\n        treeNode: CommentTreeNode? = nil,\n        inFeed: Bool = false, // flag to suppress threading/collapsing behavior\n        highlight: Bool = false,\n        depthOffset: Int = 0,\n        @ViewBuilder embeddedContent: () -> EmbeddedContent = { EmptyView() }\n    ) {\n        self.comment = comment\n        self.treeNode = treeNode\n        self.inFeed = inFeed\n        self.highlight = highlight\n        self.depthOffset = depthOffset\n        self.embeddedContent = embeddedContent()\n    }\n    \n    var depth: Int {\n        inFeed ? 0 : comment.depth - depthOffset\n    }\n    \n    var collapsed: Bool { treeNode?.collapsed ?? false }\n    \n    var compact: Bool { compactComments && reportContext == nil }\n    \n    @ViewBuilder\n    var body: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            if inFeed {\n                feedHeader\n                    .padding(.trailing, Constants.main.standardSpacing)\n            }\n            \n            HStack(spacing: 0) {\n                CommentBarView(depth: comment.depth, inFeed: inFeed)\n                \n                VStack(alignment: .leading, spacing: 0) {\n                    VStack(alignment: .leading, spacing: Constants.main.compactSpacing) {\n                        if !inFeed {\n                            authorAndMenu.padding(.top, Constants.main.standardSpacing)\n                        }\n                        \n                        if !collapsed {\n                            CommentBodyView(comment: comment)\n                                .padding(.trailing, 2)\n                            embeddedContent\n                        }\n                        \n                        if compact, !collapsed {\n                            InfoStackView(\n                                comment: comment,\n                                readouts: compactReadouts,\n                                coloredReadouts: .init(CommentBarConfiguration.ReadoutType.allCases)\n                            )\n                            .layoutPriority(1)\n                        }\n                    }\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                    .padding(.leading, 2)\n                    \n                    if !compact, !collapsed {\n                        InteractionBarView(\n                            appState: appState,\n                            navigation: navigation,\n                            comment: comment,\n                            configuration: interactionBarConfiguration,\n                            commentTreeTracker: commentTreeTracker,\n                            communityContext: communityContext,\n                            reportContext: reportContext\n                        )\n                    }\n                }\n                .padding(.bottom, collapsed || compact ? Constants.main.standardSpacing : 0)\n            }\n        }\n        .background(highlight ? palette.accent.opacity(0.2) : .clear)\n        .background(.themedSecondaryGroupedBackground)\n        .clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n        .contentShape(.interaction, .rect)\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n        .environment(\\.commentContext, comment)\n        .environment(\\.communityContext, comment.community.value)\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n    \n    var feedHeader: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            authorAndMenu\n            \n            ExpectedView(comment.post) { post in\n                FooterLinkView(title: post.title, subtitle: nil)\n                    .frame(maxWidth: .infinity)\n            }\n        }\n        .padding([.leading, .top], Constants.main.standardSpacing)\n    }\n    \n    var authorAndMenu: some View {\n        HStack(spacing: 0) {\n            ExpectedView(comment.creator) { creator in\n                FullyQualifiedLinkView(creator, labelStyle: .small)\n            } placeholder: {\n                Text(verbatim: .personPlaceholder).redacted(reason: .placeholder)\n            }\n            Spacer()\n            Group {\n                if collapsed {\n                    if let saved = comment.saved.value, let votes = comment.votes.value {\n                        Group {\n                            postTag(active: saved, icon: .lemmy.saved.representingState(active: true), color: .themedSave) +\n                            Text(verbatim: \" \") +\n                            postTag(active: votes.myVote != .none, icon: .init(votes.iconName), color: votes.iconColor)\n                        }\n                        .lineLimit(1)\n                        .font(.caption)\n                    }\n                    \n                    Image(icon: .general.expand)\n                        .frame(height: 10)\n                        .imageScale(.small)\n                } else {\n                    ellipsisMenus\n                        .frame(height: 10)\n                }\n            }\n            .padding(.leading, Constants.main.standardSpacing)\n        }\n    }\n    \n    var ellipsisMenus: some View {\n        HStack {\n            if comment.shouldShowLoadingSymbol(for: commentInteractionBar) {\n                ProgressView()\n            }\n            switch moderatorActionGrouping {\n            case .separateMenu:\n                if comment.canModerate {\n                    EllipsisMenu(icon: .lemmy.moderation, size: 24, comment: comment, type: [.moderator])\n                }\n                EllipsisMenu(size: 24, comment: comment, type: [.basic])\n            case .divider:\n                EllipsisMenu(size: 24, comment: comment)\n            }\n        }\n    }\n    \n    var interactionBarConfiguration: CommentBarConfiguration {\n        if reportContext != nil, alternateInteractionBarLayoutForReports {\n            return commentReportInteractionBar\n        }\n        return commentInteractionBar\n    }\n}\n\nstruct CommentBarView: View {\n    let depth: Int\n    var inFeed: Bool = false\n    \n    var body: some View {\n        Capsule()\n            .fill(inFeed ? .themedTertiary : .themedCommentIndentColor(depth))\n            .frame(width: 3)\n            .frame(maxHeight: .infinity)\n            .padding(.leading, 8)\n            .padding(.bottom, 8)\n            .padding(.top, inFeed ? 0 : 8)\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment, .sizeThatFitsLayout) {\n//        CommentView(comment: Comment2.mock(.generic))\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ContentLoader.swift",
    "content": "//\n//  ContentLoader.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-13.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport os\nimport Semaphore\nimport SwiftUI\n\n// TODO: Unified Community, Modlog remove this\nstruct ContentLoader<Content: View, Model: Upgradable>: View {\n    @Environment(AppState.self) var appState: AppState\n    \n    @State var proxy: ContentLoaderProxy<Model>\n    let resolveIfModelExternal: Bool\n    @ViewBuilder var content: (_ proxy: ContentLoaderProxy<Model>) -> Content\n    var upgradeOperation: ((_ model: Model, _ api: ApiClient) async throws -> Void)?\n    \n    init(\n        model: Model,\n        resolveIfModelExternal: Bool = true,\n        @ViewBuilder content: @escaping (_ proxy: ContentLoaderProxy<Model>) -> Content,\n        upgradeOperation: ((_ model: Model, _ api: ApiClient) async throws -> Void)? = nil\n    ) {\n        self._proxy = .init(wrappedValue: ContentLoaderProxy(model: model))\n        self.resolveIfModelExternal = resolveIfModelExternal\n        self.upgradeOperation = upgradeOperation\n        self.content = content\n    }\n    \n    var body: some View {\n        content(proxy)\n            .animation(.easeOut(duration: 0.2), value: proxy.model.wrappedValue is Model.MinimumRenderable)\n            .task { @MainActor in\n                if resolveIfModelExternal || !proxy.model.isUpgraded, proxy.upgradeState == .idle {\n                    await proxy.upgradeModel(\n                        api: resolveIfModelExternal ? appState.firstApi : nil,\n                        upgradeOperation: upgradeOperation\n                    )\n                }\n            }\n            .onChange(of: appState.firstApi) {\n                if proxy.upgradeState != .loading {\n                    // This code is needed here despite also being in `upgradeModel` to\n                    // ensure that `upgradeState` is changed fast enough\n                    proxy.upgradeState = .loading\n                    Task { @MainActor in\n                        await proxy.upgradeModel(api: appState.firstApi, upgradeOperation: upgradeOperation)\n                    }\n                }\n            }\n    }\n}\n\n@Observable @MainActor\nclass ContentLoaderProxy<Model: Upgradable> {\n    private let log: Logger = .mlemLogger()\n    \n    fileprivate enum UpgradeState: String {\n        case idle, loading, done, failed\n    }\n    \n    fileprivate var model: Model\n    fileprivate var upgradeState: UpgradeState = .idle\n    \n    var error: Error?\n    private let loadingSemaphore: AsyncSemaphore = .init(value: 1)\n    \n    init(model: Model) {\n        self.model = model\n    }\n    \n    var entity: (Model.MinimumRenderable)? {\n        model.wrappedValue as? Model.MinimumRenderable\n    }\n    \n    var isLoading: Bool { upgradeState == .loading }\n    \n    @MainActor\n    func upgradeModel(\n        api: ApiClient? = nil,\n        upgradeOperation: ((_ model: Model, _ api: ApiClient) async throws -> Void)?\n    ) async {\n        // critical function, only one thread allowed!\n        await loadingSemaphore.wait()\n        defer { loadingSemaphore.signal() }\n        \n        upgradeState = .loading\n        do {\n            do {\n                guard let modelApi = (model.wrappedValue as? any ContentModel)?.api else {\n                    assertionFailure()\n                    return\n                }\n                if let upgradeOperation {\n                    try await upgradeOperation(model, api ?? modelApi)\n                } else {\n                    try await model.upgrade(api: api ?? modelApi, upgradeOperation: nil)\n                }\n            } catch ApiClientError.noEntityFound {\n                log.info(\"No entity found, upgrading from local\")\n                if !model.isUpgraded {\n                    try await model.upgradeFromLocal()\n                }\n            }\n            upgradeState = .done\n        } catch ApiClientError.cancelled {\n            // if the task is cancelled, reset upgradeState--upgrade will be retried on next render\n            upgradeState = .idle\n        } catch {\n            upgradeState = .failed\n            handleError(error, silent: true)\n            self.error = error\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/CustomTabBarController.swift",
    "content": "//\n//  CustomTabBarController.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/04/2024.\n//\n\nimport Dependencies\nimport Foundation\nimport os\nimport SwiftUI\nimport Theming\n\nclass CustomTabBarController: UITabBarController, UITabBarControllerDelegate {\n    private let log: Logger = .mlemLogger()\n    \n    @Binding var selectedIndexBinding: Int\n    let swipeGestureCallback: () -> Void\n    let palette: Theming.Palette\n    \n    init(\n        selectedIndex: Binding<Int>,\n        swipeGestureCallback: @escaping () -> Void,\n        palette: Theming.Palette,\n        nibName: String? = nil,\n        bundle: Bundle? = nil\n    ) {\n        self.swipeGestureCallback = swipeGestureCallback\n        self._selectedIndexBinding = selectedIndex\n        self.palette = palette\n        super.init(nibName: nibName, bundle: bundle)\n    }\n    \n    // This is used for Storyboard, and wont ever be called as long as we dont use Storyboard\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        delegate = self\n        hidesBottomBarWhenPushed = true\n        tabBar.tintColor = UIColor(palette.accent)\n        let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressGestureTriggered(_:)))\n        tabBar.addGestureRecognizer(longPressRecognizer)\n        \n        let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipeGestureTriggered(_:)))\n        swipeGestureRecognizer.direction = .up\n        tabBar.addGestureRecognizer(swipeGestureRecognizer)\n    }\n    \n    @objc func longPressGestureTriggered(_ recognizer: UILongPressGestureRecognizer) {\n        guard recognizer.state == .began else { return }\n        guard let tabBar = recognizer.view as? UITabBar else { return }\n        guard let tabBarItems = tabBar.items else { return }\n        guard let viewControllers else { return }\n        guard tabBarItems.count == viewControllers.count else { return }\n\n        let loc = recognizer.location(in: tabBar)\n\n        for (index, item) in tabBarItems.enumerated() {\n            guard let view = item.value(forKey: \"view\") as? UIView else { continue }\n            guard view.frame.contains(loc) else { continue }\n            \n            let item: CustomTabViewHostingController?\n            if let navigationController = viewControllers[index] as? UINavigationController {\n                item = navigationController.viewControllers.first as? CustomTabViewHostingController\n            } else {\n                item = viewControllers[index] as? CustomTabViewHostingController\n            }\n            item?.item.onLongPress?()\n            break\n        }\n    }\n    \n    @objc func swipeGestureTriggered(_ recognizer: UISwipeGestureRecognizer) {\n        if !UIDevice.isIos26 {\n            swipeGestureCallback()\n        }\n    }\n    \n    func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {\n        guard !TabReselectTracker.main.blockTabSwitch else {\n            return false\n        }\n        \n        TabReselectTracker.main.reset() // reset to prevent unconsumed actions from blocking the reselect flag\n        if tabBarController.selectedViewController === viewController,\n           let item = viewController as? CustomTabViewHostingController {\n            log.debug(\"\\(item.item.title) tab re-selected\")\n            TabReselectTracker.main.signal()\n            return TabReselectTracker.main.consumers == 0\n        }\n        selectedIndexBinding = viewControllers?.firstIndex(of: viewController) ?? 0\n        return true\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/CustomTabItem.swift",
    "content": "//\n//  TabBarElement.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/04/2024.\n//\n\nimport SwiftUI\n\nstruct CustomTabItem {\n    var content: AnyView\n    \n    var title: String\n    var image: UIImage?\n    var selectedImage: UIImage?\n    var badge: String?\n    \n    var onLongPress: (() -> Void)?\n    \n    @_disfavoredOverload // This ensures that the other initialiser takes priority\n    init(\n        title: String,\n        image: UIImage?,\n        selectedImage: UIImage? = nil,\n        badge: String? = nil,\n        onLongPress: (() -> Void)? = nil,\n        @ViewBuilder content: () -> some View\n    ) {\n        self.title = title\n        self.image = image\n        self.selectedImage = selectedImage ?? image\n        self.onLongPress = onLongPress\n        self.badge = badge\n        self.content = AnyView(content())\n    }\n    \n    init(\n        title: LocalizedStringResource,\n        image: UIImage?,\n        selectedImage: UIImage? = nil,\n        badge: String? = nil,\n        onLongPress: (() -> Void)? = nil,\n        @ViewBuilder content: () -> some View\n    ) {\n        self.init(\n            title: String(localized: title),\n            image: image,\n            selectedImage: selectedImage,\n            badge: badge,\n            onLongPress: onLongPress,\n            content: content\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/CustomTabView.swift",
    "content": "//\n//  UITabBarWrapper.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/04/2024.\n//\n\nimport Foundation\nimport SwiftUI\nimport Theming\n\nstruct CustomTabView: UIViewControllerRepresentable {\n    let tabs: [CustomTabItem]\n    let swipeGestureCallback: () -> Void\n    \n    @Binding var selectedIndex: Int\n    \n    init(selectedIndex: Binding<Int>, tabs: [CustomTabItem], onSwipeUp: @escaping () -> Void) {\n        self.tabs = tabs\n        self.swipeGestureCallback = onSwipeUp\n        self._selectedIndex = selectedIndex\n    }\n    \n    func makeUIViewController(\n        context: UIViewControllerRepresentableContext<CustomTabView>\n    ) -> UITabBarController {\n        let tabBarController = CustomTabBarController(\n            selectedIndex: $selectedIndex,\n            swipeGestureCallback: swipeGestureCallback,\n            palette: context.environment.palette\n        )\n        tabBarController.viewControllers = tabs.enumerated().map { CustomTabViewHostingController(item: $1, index: $0) }\n        \n        return tabBarController\n    }\n    \n    func updateUIViewController(\n        _ uiViewController: UITabBarController,\n        context: UIViewControllerRepresentableContext<CustomTabView>\n    ) {\n        if let controller = uiViewController as? CustomTabBarController {\n            Task.detached { @MainActor in\n                for (tabData, tabBarItem) in zip(tabs, controller.tabBar.items ?? []) {\n                    tabBarItem.title = tabData.title\n                    \n                    tabBarItem.badgeValue = tabData.badge\n                    tabBarItem.image = tabData.image\n                    tabBarItem.selectedImage = tabData.selectedImage\n                    tabBarItem.badgeColor = UIColor(ThemedColor.themedWarning.resolve(in: context.environment))\n                }\n                \n                controller.tabBar.tintColor = UIColor(ThemedColor.themedAccent.resolve(in: context.environment))\n            }\n        }\n        \n        withObservationTracking {\n            _ = AppState.main.contentViewTab\n        } onChange: {\n            if let controller = uiViewController as? CustomTabBarController {\n                Task.detached { @MainActor in\n                    controller.selectedIndex = selectedIndex\n                }\n            }\n        }\n    }\n    \n    func makeCoordinator() -> Coordinator {\n        Coordinator(self)\n    }\n    \n    class Coordinator: NSObject {\n        var parent: CustomTabView\n        \n        init(_ controller: CustomTabView) {\n            self.parent = controller\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/CustomTabViewHostingController.swift",
    "content": "//\n//  TabBarHostingController.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/04/2024.\n//\n\nimport Foundation\nimport SwiftUI\n\nclass CustomTabViewHostingController: UIHostingController<AnyView> {\n    let item: CustomTabItem\n    \n    init(item: CustomTabItem, index: Int) {\n        self.item = item\n        super.init(rootView: item.content)\n        \n        self.tabBarItem = UITabBarItem(\n            title: item.title,\n            image: nil,\n            selectedImage: nil\n        )\n    }\n    \n    @available(*, unavailable)\n    @MainActor dynamic required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/EllipsisMenu.swift",
    "content": "//\n//  EllipsisMenu.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-24.\n//\n\nimport Actions\nimport Foundation\nimport Icons\nimport SwiftUI\n\nstruct EllipsisMenu<Content: View>: View {\n    let content: Content\n    let icon: Icon\n    let size: CGFloat\n    \n    init(icon: Icon = .general.menu, size: CGFloat, @ViewBuilder content: @escaping () -> Content) {\n        self.icon = icon\n        self.size = size\n        self.content = content()\n    }\n    \n    var body: some View {\n        Menu {\n            content\n        } label: {\n            Image(icon: icon)\n                .frame(width: 24, height: size)\n                .contentShape(.rect)\n        }\n        .popupAnchor()\n        .buttonStyle(.empty)\n        .onTapGesture {} // prevent NavigationLink from disabling menu (thanks Swift)\n    }\n}\n\nextension EllipsisMenu {\n    init(\n        icon: Icon = .general.menu,\n        size: CGFloat,\n        @ActionBuilder actions: @escaping () -> [any Action]\n    ) where Content == MenuButtons {\n        self.icon = icon\n        self.size = size\n\n        self.content = MenuButtons(actions: actions)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/EndOfFeedView.swift",
    "content": "//\n//  EndOfFeedView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-19.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct EndOfFeedViewContent {\n    // This is a `LocalizedStringResource` because different languages\n    // may want to change this icon in order to have it match the locale-specific idiom\n    let icon: LocalizedStringResource\n    let message: LocalizedStringResource\n}\n\nenum EndOfFeedViewType {\n    case hobbit, cartoon, turtle\n    \n    var viewContent: EndOfFeedViewContent {\n        switch self {\n        case .hobbit:\n            return EndOfFeedViewContent(\n                icon: .init(\n                    \"end.of.feed.icon.1\",\n                    defaultValue: \"figure.climbing\",\n                    // swiftlint:disable:next line_length\n                    comment: \"This is the key for an icon that appears next to the \\\"I think I've found the bottom!\\\" text. It is localized so that you can change the icon to fit better with your translation of the text.\"\n                ),\n                message: \"I think I've found the bottom!\"\n            )\n        case .cartoon:\n            return EndOfFeedViewContent(\n                icon: .init(\n                    \"end.of.feed.icon.2\",\n                    defaultValue: \"figure.wave\",\n                    // swiftlint:disable:next line_length\n                    comment: \"This is the key for an icon that appears next to the \\\"That's all, folks!\\\" text. It is localized so that you can change the icon to fit better with your translation of the text.\"\n                ),\n                message: \"That's all, folks!\"\n            )\n        case .turtle:\n            return EndOfFeedViewContent(\n                icon: .init(\n                    \"end.of.feed.icon.3\",\n                    defaultValue: \"tortoise\",\n                    // swiftlint:disable:next line_length\n                    comment: \"This is the key for an icon that appears next to the \\\"It's turtles all the way down\\\" text. It is localized so that you can change the icon to fit better with your translation of the text.\"\n                ),\n                message: \"It's turtles all the way down\"\n            )\n        }\n    }\n}\n\nstruct EndOfFeedView: View {\n    @Setting(\\.dev_developerMode) var developerMode\n    \n    @State var showLoadMore: Bool = false\n    \n    let loadingState_: FeedLoadingState?\n    let viewType: EndOfFeedViewType\n    let feedLoader: (any FeedLoading)?\n    \n    var loadingState: FeedLoadingState {\n        assert(loadingState_ != nil || feedLoader != nil, \"either loadingState_ or feedLoader must be defined\")\n        return loadingState_ ?? feedLoader?.loadingState ?? .done\n    }\n    \n    @_disfavoredOverload\n    init(loadingState: LoadingState, viewType: EndOfFeedViewType) {\n        self.init(loadingState: .init(from: loadingState), viewType: viewType)\n    }\n\n    init(loadingState: FeedLoadingState, viewType: EndOfFeedViewType) {\n        self.loadingState_ = loadingState\n        self.feedLoader = nil\n        self.viewType = viewType\n    }\n    \n    init(feedLoader: any FeedLoading, viewType: EndOfFeedViewType) {\n        self.loadingState_ = nil\n        self.feedLoader = feedLoader\n        self.viewType = viewType\n    }\n    \n    var body: some View {\n        Group {\n            switch loadingState {\n            case .idle:\n                Group {\n                    if showLoadMore, let feedLoader {\n                        Button(\"Load More\") {\n                            Task {\n                                do {\n                                    try await feedLoader.loadMoreItems()\n                                } catch {\n                                    handleError(error)\n                                }\n                            }\n                        }\n                        .tint(.themedAccent)\n                        .buttonStyle(.bordered)\n                    } else {\n                        Group {\n                            if developerMode {\n                                Text(verbatim: \"IDLE\")\n                            } else {\n                                ProgressView()\n                            }\n                        }\n                        .onAppear {\n                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                                if loadingState == .idle {\n                                    showLoadMore = true\n                                }\n                            }\n                        }\n                    }\n                }\n            case .loading, .initial:\n                ProgressView()\n            case .done:\n                HStack {\n                    Image(systemName: .init(localized: viewType.viewContent.icon))\n                    Text(viewType.viewContent.message)\n                }\n                .foregroundStyle(.themedSecondary)\n            }\n        }\n        .frame(minHeight: 100)\n        .onChange(of: loadingState, initial: true) {\n            if loadingState != .idle {\n                showLoadMore = false\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ErrorView.swift",
    "content": "//\n//  Error View.swift\n//  Mlem\n//\n//  Created by David Bureš on 19.06.2022.\n//\n\nimport Combine\nimport MlemMiddleware\nimport SwiftUI\nimport UniformTypeIdentifiers\n\nstruct ErrorView: View {\n    @Setting(\\.dev_developerMode) var developerMode\n    \n    @State var errorDetails: ErrorDetails\n    \n    @State private var showingFullError: Bool = false\n    @State private var refreshInProgress: Bool = false\n    \n    var timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common)\n        .autoconnect()\n    \n    init(_ errorDetails: ErrorDetails) {\n        _errorDetails = State(wrappedValue: errorDetails)\n    }\n\n    var body: some View {\n        VStack(spacing: 15) {\n            if showingFullError {\n                errorDetails(errorDetails.errorText())\n            } else {\n                if let icon = errorDetails.icon {\n                    Image(icon: icon)\n                        .resizable()\n                        .aspectRatio(contentMode: .fit)\n                        .frame(height: 50)\n                }\n                Text(errorDetails.title ?? \"Something went wrong.\")\n                    .font(.title3.bold())\n                    .foregroundStyle(.themedPrimary)\n                \n                if let body = errorDetails.body {\n                    Text(body)\n                        .multilineTextAlignment(.center)\n                }\n                \n                if !errorDetails.autoRefresh, let refresh = errorDetails.refresh {\n                    Button {\n                        Task {\n                            refreshInProgress = true\n                            if await refresh() {\n                                timer.upstream.connect().cancel()\n                            }\n                            refreshInProgress = false\n                        }\n                    } label: {\n                        HStack(spacing: 10) {\n                            Text(errorDetails.buttonText ?? \"Try Again\")\n                            if refreshInProgress {\n                                ProgressView()\n                            }\n                        }\n                    }\n                    .tint(.themedSecondary)\n                    .buttonStyle(.bordered)\n                    .animation(.default, value: refreshInProgress)\n                }\n            }\n            \n            if errorDetails.error != nil, errorDetails.title == nil || developerMode {\n                Button(showingFullError ? \"Hide Details\" : \"Show Details\") {\n                    showingFullError.toggle()\n                }\n                .buttonStyle(.plain)\n                .foregroundStyle(.themedTertiary)\n            }\n        }\n        .padding()\n        .foregroundColor(.secondary)\n        .onDisappear {\n            timer.upstream.connect().cancel()\n        }\n        \n        .onReceive(timer) { _ in\n            if errorDetails.autoRefresh, let refresh = errorDetails.refresh {\n                Task {\n                    if await refresh() {\n                        timer.upstream.connect().cancel()\n                    }\n                }\n            }\n        }\n        \n        .animation(.default, value: showingFullError)\n        .padding()\n    }\n        \n    @ViewBuilder\n    func errorDetails(_ errorText: String) -> some View {\n        VStack {\n            Text(errorText)\n                .foregroundStyle(.red)\n            Divider()\n            Button {\n                UIPasteboard.general.setValue(\n                    errorText,\n                    forPasteboardType: UTType.plainText.identifier\n                )\n            } label: {\n                Label(\"Copy\", systemImage: \"square.on.square\")\n            }\n            .buttonStyle(.plain)\n        }\n        .padding(Constants.main.standardSpacing)\n        .background(Color(.secondarySystemGroupedBackground))\n        .clipShape(RoundedRectangle(cornerRadius: Constants.main.standardSpacing))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpandedPost/CommentPage.swift",
    "content": "//\n//  CommentPage.swift\n//  Mlem\n//\n//  Created by Sjmarf on 27/09/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct CommentPage: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.palette) var palette\n    @Environment(\\.dismiss) var dismiss\n    \n    let comment: Comment\n    let initialComments: [Comment]?\n    @State var tracker: CommentTreeTracker\n    let showViewPostButton: Bool\n    let exposeRemovedContent: Bool\n\n    init(\n        comment: Comment,\n        initialComments: [Comment]?,\n        showViewPostButton: Bool = false,\n        exposeRemovedContent: Bool = false\n    ) {\n        self.comment = comment\n        self.showViewPostButton = showViewPostButton\n        self.initialComments = initialComments\n        self.exposeRemovedContent = exposeRemovedContent\n        self._tracker = .init(wrappedValue: .init(root: .comment(comment, parentCount: 1)))\n    }\n    \n    var body: some View {\n        ExpectedView(comment.post) { post in\n            content(post: post)\n        }\n    }\n    \n    // swiftlint:disable:next function_body_length\n    func content(post: Post) -> some View {\n        ExpandedPostView(\n            post: post,\n            tracker: $tracker,\n            scrollTargetedComment: comment\n        ) {\n            if showViewPostButton || tracker.nodes.first?.comment.depth != 0 {\n                HStack(spacing: Constants.main.standardSpacing) {\n                    if tracker.nodes.first?.comment.depth != 0 {\n                        Button {\n                            tracker.root = .comment(comment, parentCount: currentDepth + 1)\n                            Task {\n                                await tracker.refresh()\n                            }\n                        } label: {\n                            HStack {\n                                Text(\"Show Parent\")\n                                if tracker.loadingState == .loading {\n                                    ProgressView()\n                                } else {\n                                    Image(systemName: \"chevron.up\")\n                                }\n                            }\n                            .animation(.easeOut(duration: 0.1), value: tracker.loadingState == .loading)\n                        }\n                    }\n                    if showViewPostButton {\n                        Button {\n                            navigation.push(.post(post))\n                        } label: {\n                            HStack {\n                                Text(\"View All\")\n                                Image(icon: .general.forward)\n                            }\n                        }\n                    }\n                }\n                .buttonStyle(.capsule)\n                .padding(.horizontal, Constants.main.standardSpacing)\n            }\n        }\n        .refreshable {\n            _ = await Task { @MainActor in\n                await tracker.refresh()\n            }.value\n        }\n        .themedGroupedBackground()\n        .onAppear {\n            Task {\n                await tracker.load()\n            }\n        }\n        .environment(\\.exposeRemovedContent, exposeRemovedContent)\n    }\n    \n    var currentDepth: Int {\n        switch tracker.root {\n        case let .comment(_, currentDepth): currentDepth\n        default: 0\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpandedPost/CommentStubResolutionPage.swift",
    "content": "//\n//  CommentStubResolutionPage.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-01-11.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct CommentStubResolutionPage: View {\n    @Environment(NavigationLayer.self) var navigation\n    \n    let stub: CommentStub\n    let comments: [Comment]?\n    let showViewPostButton: Bool\n    let exposeRemovedContent: Bool\n    \n    @State var upgradeError: Error?\n    \n    var body: some View {\n        content\n            .themedGroupedBackground()\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        if let upgradeError {\n            ErrorView(.init(\n                error: upgradeError,\n                refresh: fetchComment\n            ))\n        } else {\n            ProgressView()\n                .task {\n                    await fetchComment()\n                }\n        }\n    }\n    \n    @discardableResult\n    func fetchComment() async -> Bool {\n        do {\n            // TODO: NOW make this smoother\n            try await navigation.replace(.comment(\n                stub.asComment(),\n                comments: comments,\n                showViewPostButton: showViewPostButton,\n                exposeRemovedContent: exposeRemovedContent\n            ))\n            return true\n        } catch {\n            upgradeError = error\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpandedPost/CommentTreeTracker.swift",
    "content": "//\n//  ExpandedPostView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 12/07/2024.\n//\n\nimport MlemMiddleware\nimport os\nimport SwiftUI\n\n@Observable\nclass CommentTreeTracker: Hashable {\n    private let log: Logger = .mlemLogger()\n    \n    enum Root {\n        case post(Post)\n        case comment(Comment, parentCount: Int)\n        \n        var wrappedValue: any InteractableProviding & ActorIdentifiable {\n            switch self {\n            case let .post(post): post\n            case let .comment(comment, _): comment\n            }\n        }\n        \n        var depth: Int {\n            switch self {\n            case .post: -1\n            case let .comment(comment, parentCount): max(0, comment.depth - parentCount)\n            }\n        }\n    }\n    \n    private(set) var nodes: [CommentTreeNode] = []\n    private(set) var nodesKeyedByActorId: [ActorIdentifier: CommentTreeNode] = [:]\n    \n    var loadingState: LoadingState = .idle\n    var errorDetails: ErrorDetails?\n    \n    var root: Root\n    \n    var sort: CommentSortType = .init(Settings.get(\\.comment_defaultSort))\n    \n    init(root: Root) {\n        self.root = root\n    }\n    \n    var proposedDepthOffset: Int {\n        switch root {\n        case .comment:\n            if let first = nodes.first, first.comment.depth > 0 {\n                return first.comment.depth - 1\n            }\n            return 0\n        default: return 0\n        }\n    }\n    \n    private var appState: AppState { .main }\n\n    func getNode(actorId: ActorIdentifier) -> CommentTreeNode? {\n        nodesKeyedByActorId[actorId]\n    }\n\n    func hasNode(actorId: ActorIdentifier) -> Bool {\n        return nodesKeyedByActorId.keys.contains(actorId)\n    }\n    \n    @MainActor\n    func load(ensuringPresenceOf ensuredComment: (any CommentResolvable)? = nil) async {\n        guard loadingState == .idle else { return }\n        loadingState = .loading\n        do {\n            var newComments = try await fetchComments(page: 1)\n            if let ensuredComment {\n                let comment = try await ensuredComment.asComment()\n                let api = root.wrappedValue.api\n                if !nodesKeyedByActorId.keys.contains(comment.actorId) {\n                    // Find the first parent of the ensured comment that isn't in `newComments`.\n                    // This will be the starting point for the second page of comments to load.\n                    let idsToSearch = comment.parentCommentIds + [comment.id]\n                    let firstAbsentParentId = idsToSearch.first(\n                        where: { id in !newComments.contains(where: { $0.id == id }) }\n                    )\n                    if let firstAbsentParentId {\n                        let extraComments = try await api.getComments(\n                            parentId: firstAbsentParentId,\n                            sort: sort,\n                            page: 1,\n                            maxDepth: 8,\n                            limit: 999\n                        )\n                        newComments.append(contentsOf: extraComments)\n                    }\n                }\n            }\n            if let first = newComments.first, first.api != root.wrappedValue.api {\n                resolveCommentTree(comments: newComments)\n            } else {\n                await buildCommentTree(comments: newComments)\n            }\n            loadingState = .done\n            errorDetails = nil\n        } catch {\n            handleFailure(error)\n        }\n    }\n    \n    @MainActor\n    private func fetchComments(page: Int) async throws -> [Comment] {\n        switch root {\n        case let .post(post):\n            return try await post.getComments(\n                sort: sort,\n                page: page,\n                maxDepth: Settings.get(\\.comment_maxDepth),\n                limit: 50\n            )\n        case let .comment(comment, parentCount):\n            return try await comment.getChildren(\n                sort: sort,\n                includedParentCount: parentCount,\n                page: page,\n                maxDepth: min(8, Settings.get(\\.comment_maxDepth)) + parentCount,\n                limit: 999\n            )\n        }\n    }\n    \n    private func handleFailure(_ error: Error) {\n        var details = handleErrorWithDetails(error)\n        details?.refresh = {\n            await self.load()\n            return self.loadingState == .done\n        }\n        errorDetails = details\n        loadingState = .idle\n    }\n    \n    @MainActor\n    func refresh() async {\n        errorDetails = nil\n        loadingState = .idle\n        await load()\n    }\n    \n    func clear() {\n        nodes.removeAll()\n        nodesKeyedByActorId.removeAll()\n        loadingState = .idle\n    }\n    \n    func insertCreatedComment(_ comment: Comment, parent: Comment? = nil) {\n        let wrapper = CommentTreeNode(comment)\n        nodesKeyedByActorId[comment.actorId] = wrapper\n        if let parent {\n            assert(!comment.parentCommentIds.isEmpty)\n            nodesKeyedByActorId[parent.actorId]?.addChild(wrapper)\n        } else {\n            assert(comment.parentCommentIds.isEmpty)\n            nodes.prepend(wrapper)\n        }\n    }\n    \n    @MainActor\n    func insertAdditionalComments(comments newComments: [Comment]) async {\n        await buildCommentTree(comments: newComments, clear: false)\n    }\n    \n    func getThread(preceding target: Comment, limit: Int) -> [Comment] {\n        var cur = nodesKeyedByActorId[target.actorId]\n        var ret: [Comment] = .init()\n        while ret.count < limit, let curNode = cur {\n            ret.prepend(curNode.comment)\n            cur = curNode.parent\n        }\n        \n        assert(ret.count > 0, \"Could not build thread from \\(target.actorId)\")\n        return ret\n    }\n    \n    @MainActor\n    private func buildCommentTree(comments newComments: [Comment], clear: Bool = true) async {\n        var output: [CommentTreeNode] = clear ? [] : nodes\n        var commentsKeyedById: [Int: CommentTreeNode] = [:]\n        var commentsKeyedByActorId: [ActorIdentifier: CommentTreeNode] = clear ? [:] : nodesKeyedByActorId\n\n        let sortedComments = newComments.sorted { $0.depth < $1.depth }\n        \n        for comment in sortedComments {\n            if commentsKeyedByActorId.keys.contains(comment.actorId) {\n                commentsKeyedById[comment.id] = commentsKeyedByActorId[comment.actorId]\n                continue\n            }\n            let wrapper: CommentTreeNode = .init(comment)\n            commentsKeyedById[comment.id] = wrapper\n            commentsKeyedByActorId[comment.actorId] = wrapper\n            if let parentId = comment.parentCommentIds.last, comment.depth > root.depth {\n                if let parent = commentsKeyedById[parentId] {\n                    parent.addChild(wrapper)\n                }\n            } else {\n                output.append(wrapper)\n            }\n        }\n        nodes = output\n        nodesKeyedByActorId = commentsKeyedByActorId\n    }\n\n    private func resolveCommentTree(comments newComments: [Comment]) {\n        var commentsKeyedById: [Int: CommentTreeNode] = [:]\n        \n        for comment in newComments {\n            if let existing = nodesKeyedByActorId[comment.actorId] {\n                existing.comment = comment\n                commentsKeyedById[comment.id] = existing\n            } else {\n                let wrapper: CommentTreeNode = .init(comment)\n                commentsKeyedById[comment.id] = wrapper\n                nodesKeyedByActorId[comment.actorId] = wrapper\n                if let parentId = comment.parentCommentIds.last {\n                    if let parent = commentsKeyedById[parentId] {\n                        parent.addChild(wrapper)\n                    } else {\n                        assertionFailure(\"This should never happen because the API returns comments in order of depth asc.\")\n                    }\n                } else {\n                    nodes.append(wrapper)\n                }\n            }\n        }\n    }\n    \n    static func == (lhs: CommentTreeTracker, rhs: CommentTreeTracker) -> Bool {\n        lhs === rhs\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(ObjectIdentifier(self))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpandedPost/ExpandedPostHistoryTracker.swift",
    "content": "//\n//  ExpandedPostHistoryTracker.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-22.\n//\n\nimport Foundation\nimport MlemMiddleware\n\n// This needs to be Observable in order for it to work in the envrionment,\n// but we aren't actually using any Observable properties at present\n@Observable\nclass ExpandedPostHistoryTracker {\n    @ObservationIgnored private var postActorIds: [ActorIdentifier] = []\n    @ObservationIgnored private var postToCommentMap: [ActorIdentifier: ActorIdentifier] = [:]\n    \n    func insert(postActorId: ActorIdentifier, commentActorId: ActorIdentifier) {\n        if let index = postActorIds.firstIndex(of: postActorId) {\n            postActorIds.remove(at: index)\n        }\n        postActorIds.append(postActorId)\n        postToCommentMap[postActorId] = commentActorId\n        \n        if postActorIds.count > 10 {\n            let removedId = postActorIds.removeFirst()\n            postToCommentMap[removedId] = nil\n        }\n    }\n    \n    func retrieve(for postActorId: ActorIdentifier) -> ActorIdentifier? { postToCommentMap[postActorId] }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView+Logic.swift",
    "content": "//\n//  ExpandedPostView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 24/08/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension ExpandedPostView {\n    struct AnchorsKey: PreferenceKey {\n        // swiftlint:disable:next nesting\n        typealias Value = [ActorIdentifier?: Anchor<CGPoint>]\n\n        static var defaultValue: Value { [:] }\n\n        static func reduce(value: inout Value, nextValue: () -> Value) {\n            value.merge(nextValue()) { $1 }\n        }\n    }\n    \n    enum PreviousVisitRecord {\n        case firstVisit\n        case revisit(topVisibleCommentAtLastVisit: ActorIdentifier)\n        \n        var isRevisit: Bool {\n            switch self {\n            case .revisit: true\n            default: false\n            }\n        }\n        \n        var commentActorId: ActorIdentifier? {\n            switch self {\n            case let .revisit(topVisibleCommentAtLastVisit: value): value\n            default: nil\n            }\n        }\n    }\n\n    enum CommentTreeViewType: Hashable {\n        case comment(CommentTreeNode)\n        case unloadedComments(comment: CommentTreeNode, count: Int)\n        \n        func hash(into hasher: inout Hasher) {\n            switch self {\n            case let .comment(comment):\n                hasher.combine(1)\n                hasher.combine(comment.actorId)\n            case let .unloadedComments(comment, _):\n                hasher.combine(2)\n                hasher.combine(comment.actorId)\n            }\n        }\n    }\n    \n    var hasNoComments: Bool {\n        if tracker.loadingState == .done {\n            return tracker.nodesKeyedByActorId.count == 0\n        }\n        return (post.commentCount.value ?? -1) == 0\n    }\n    \n    var showLoadingSymbol: Bool {\n        // Don't need to show ProgressView if there's nothing to scroll to\n        if scrollTargetedComment == nil { return false }\n        return !scrolledToScrollTargetedComment\n    }\n    \n    func showScrollToLastVisitButton(post: Post) -> Bool {\n        guard (post.commentCount.value_ ?? 0) > 10 else { return false }\n        var commentId = previousVisitRecord?.commentActorId\n        if topVisibleItem.isAtPost, commentId == nil {\n            commentId = topVisibleItem.furthestVisitedComment\n        }\n        guard let commentId else { return false }\n        let nodes = tracker.nodes.reduce([]) { $0 + $1.tree(hideIfCollapsed: false) }\n        let index = nodes.firstIndex { $0.actorId == commentId }\n        guard let index else { return false }\n        return index > 1\n    }\n    \n    func togglePostCollapsed(post: Post, scrollProxy: ScrollViewProxy) {\n        withAnimation(UIAccessibility.isReduceMotionEnabled ? nil : .default) {\n            postCollapsed.toggle()\n            if postCollapsed {\n                scrollProxy.scrollTo(post.actorId)\n            }\n        }\n    }\n    \n    func generateViewTree(for nodes: [CommentTreeNode]) -> [CommentTreeViewType] {\n        nodes.reduce([]) { $0 + generateViewTree(for: $1) }\n    }\n    \n    func generateViewTree(for node: CommentTreeNode) -> [CommentTreeViewType] {\n        let comment = node.comment\n        if comment.shouldHideInFeed { return [] }\n        if node.collapsed { return [.comment(node)] }\n        var output: [CommentTreeViewType] = node.children.reduce([.comment(node)]) { $0 + generateViewTree(for: $1) }\n        let directChildCount = node.children.reduce(comment.commentCount.value_ ?? 0) { $0 - ($1.comment.commentCount.value_ ?? 0) }\n        if node.children.count < directChildCount {\n            output.append(.unloadedComments(comment: node, count: (comment.commentCount.value_ ?? 0) - output.count))\n        }\n        return output\n    }\n\n    func topCommentRow(of anchors: AnchorsKey.Value, in proxy: GeometryProxy) -> ActorIdentifier? {\n        var yBest = CGFloat.infinity\n        var ret: ActorIdentifier?\n        for (row, anchor) in anchors {\n            let y = proxy[anchor].y\n            guard y >= 0, y < yBest else { continue }\n            ret = row\n            yBest = y\n        }\n        return ret\n    }\n    \n    func updateAnchors(_ anchors: AnchorsKey.Value, in proxy: GeometryProxy) {\n        topVisibleItem.wrappedValue = topCommentRow(of: anchors, in: proxy)\n        if (topVisibleItem.wrappedValue == post.actorId) != topVisibleItem.isAtPost {\n            topVisibleItem.isAtPost.toggle()\n        }\n        updateHistory()\n    }\n    \n    private func updateHistory() {\n        if let commentActorId = topVisibleItem.wrappedValue, topVisibleItem.wrappedValue != post.actorId {\n            expandedPostHistoryTracker.insert(postActorId: post.actorId, commentActorId: commentActorId)\n            if let furthestVisitedComment = topVisibleItem.furthestVisitedComment {\n                let nodes = tracker.nodes.reduce([]) { $0 + $1.tree(hideIfCollapsed: false) }\n                let furthestVisitedCommentIndex = nodes.firstIndex { $0.actorId == furthestVisitedComment }\n                let newVisitedCommentIndex = nodes.firstIndex { $0.actorId == commentActorId }\n                if let furthestVisitedCommentIndex, let newVisitedCommentIndex {\n                    if furthestVisitedCommentIndex > newVisitedCommentIndex { return }\n                }\n            }\n            topVisibleItem.furthestVisitedComment = commentActorId\n        }\n    }\n    \n    func scrollToLastVisitedPosition() {\n        if let furthestVisitedComment = topVisibleItem.furthestVisitedComment {\n            jumpButtonTarget = furthestVisitedComment\n            return\n        }\n        \n        switch previousVisitRecord {\n        case let .revisit(topVisibleCommentAtLastVisit: topVisibleCommentAtLastVisit):\n            jumpButtonTarget = topVisibleCommentAtLastVisit\n        default:\n            break\n        }\n    }\n    \n    func scrollToNextComment() {\n        if let topVisibleItem = topVisibleItem.wrappedValue {\n            if topVisibleItem == post.actorId, let first = tracker.nodes.first {\n                jumpButtonTarget = first.actorId\n                return\n            }\n            if let comment = tracker.nodesKeyedByActorId[topVisibleItem] {\n                if let topLevelIndex = tracker.nodes.firstIndex(of: comment.topParent) {\n                    guard topLevelIndex + 1 < tracker.nodes.count else { return }\n                    jumpButtonTarget = tracker.nodes[topLevelIndex + 1].actorId\n                }\n            }\n        }\n    }\n    \n    func scrollToPreviousComment() {\n        if let topVisibleItem = topVisibleItem.wrappedValue, topVisibleItem != post.actorId {\n            if let comment = tracker.nodesKeyedByActorId[topVisibleItem] {\n                if var topLevelIndex = tracker.nodes.firstIndex(of: comment.topParent) {\n                    if topLevelIndex < 0 || comment == tracker.nodes.first {\n                        jumpButtonTarget = post.actorId\n                    } else {\n                        if comment.parent == nil { topLevelIndex -= 1 }\n                        jumpButtonTarget = tracker.nodes[topLevelIndex].actorId\n                    }\n                } else {\n                    assertionFailure()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView+Views.swift",
    "content": "//\n//  ExpandedPostView+Views.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/10/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension ExpandedPostView {\n    @ViewBuilder\n    var noCommentsView: some View {\n        VStack(spacing: 5) {\n            Image(icon: .lemmy.noContent)\n                .font(.title)\n                .foregroundStyle(.tertiary)\n            Text(\"No comments found\")\n                .fontWeight(.semibold)\n        }\n        .multilineTextAlignment(.center)\n        .foregroundStyle(.themedSecondary)\n        .frame(maxWidth: .infinity)\n    }\n    \n    @ViewBuilder\n    func commentTree(tracker: CommentTreeTracker, scrollProxy: ScrollViewProxy) -> some View {\n        ForEach(generateViewTree(for: tracker.nodes), id: \\.hashValue) { item in\n            Group {\n                switch item {\n                case let .comment(node):\n                    nodeView(node: node, depthOffset: tracker.proposedDepthOffset, scrollProxy: scrollProxy)\n                case let .unloadedComments(comment, _):\n                    MoreRepliesButton(tracker: tracker, commentTreeNode: comment)\n                }\n            }\n            .padding(.horizontal, Constants.main.standardSpacing)\n            .padding(.top, compactComments ? Constants.main.halfSpacing : Constants.main.standardSpacing)\n        }\n    }\n    \n    @ViewBuilder\n    func nodeView(node: CommentTreeNode, depthOffset: Int, scrollProxy: ScrollViewProxy) -> some View {\n        let comment = node.comment\n        CommentView(\n            comment: comment,\n            treeNode: node,\n            highlight: [scrollTargetedComment?.actorId, highlightedComment?.actorId].contains(comment.actorId),\n            depthOffset: depthOffset\n        )\n        .onTapGesture {\n            if tapCommentsToCollapse {\n                withAnimation(UIAccessibility.isReduceMotionEnabled ? nil : .default) {\n                    node.collapsed.toggle()\n                }\n            }\n        }\n        .onChange(of: node.collapsed) { _, isCollapsed in\n            guard isCollapsed else { return }\n            \n            withAnimation(UIAccessibility.isReduceMotionEnabled ? nil : .default) {\n                scrollProxy.scrollTo(comment.actorId)\n            }\n        }\n        .quickSwipes(comment: comment, configuration: commentInteractionBar)\n        .contextMenu(comment: comment)\n        .popupAnchor()\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        .transition(.move(edge: .top).combined(with: .opacity))\n        .zIndex(1000 - Double(comment.depth))\n        .anchorPreference(\n            key: AnchorsKey.self,\n            value: .center\n        ) { [comment.actorId: $0] }\n        .padding(.leading, CGFloat(comment.depth - depthOffset) * 10)\n        .id(comment.actorId)\n    }\n    \n    @ViewBuilder\n    func sortPicker(tracker: CommentTreeTracker) -> some View {\n        Menu(\"Sort\", icon: tracker.sort.icon) {\n            ForEach(CommentSortType.legacyCases, id: \\.self) { item in\n                if post.api.supports(.commentSortType(item), defaultValue: true) {\n                    Toggle(\n                        item.label(timeRangeFormat: .topOnly),\n                        icon: item.icon,\n                        isOn: .init(\n                            get: { tracker.sort == item },\n                            set: { _ in\n                                tracker.sort = item\n                                tracker.clear()\n                                Task { await tracker.load() }\n                            }\n                        )\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView.swift",
    "content": "//\n//  ExpandedPostView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 03/09/2024.\n//\n\nimport Actions\nimport MlemMiddleware\nimport SwiftUI\n\n@Observable\nclass TopVisibleItemContainer {\n    // This doesn't need to trigger view updates\n    @ObservationIgnored var wrappedValue: ActorIdentifier?\n    var furthestVisitedComment: ActorIdentifier?\n    var isAtPost: Bool = true\n}\n\nstruct ExpandedPostView<Content: View>: View {\n    @Environment(AppState.self) var appState\n    @Environment(ExpandedPostHistoryTracker.self) var expandedPostHistoryTracker\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.palette) var palette\n    @Environment(\\.dismiss) var dismiss\n    \n    @Setting(\\.comment_jumpButton) var jumpButton\n    @Setting(\\.comment_compact) var compactComments\n    @Setting(\\.post_gestures_tapToCollapse) var tapPostsToCollapse\n    @Setting(\\.comment_gestures_tapToCollapse) var tapCommentsToCollapse\n    @Setting(\\.interactionBar_post) var postInteractionBar\n    @Setting(\\.interactionBar_comment) var commentInteractionBar\n\n    @State var post: Post\n    let highlightedComment: Comment?\n    let content: Content\n    @State var isLoading: Bool = false\n    \n    @Binding var tracker: CommentTreeTracker\n    @State var scrollTargetedComment: Comment?\n\n    @State var scrolledToScrollTargetedComment: Bool = false\n    @State var jumpButtonTarget: ActorIdentifier?\n    @State var topVisibleItem: TopVisibleItemContainer = .init()\n    @State var postCollapsed: Bool = false\n    \n    @State var previousVisitRecord: PreviousVisitRecord?\n    \n    init(\n        post: Post,\n        tracker: Binding<CommentTreeTracker>?,\n        highlightedComment: Comment? = nil,\n        scrollTargetedComment: Comment? = nil,\n        @ViewBuilder content: () -> Content = { EmptyView() }\n    ) {\n        self.post = post\n        self.highlightedComment = highlightedComment\n        self.content = content()\n        self._tracker = tracker ?? .constant(.init(root: .post(post)))\n        self._scrollTargetedComment = .init(wrappedValue: scrollTargetedComment)\n    }\n    \n    var body: some View {\n        // Using a `ZStack` here rather than `if`/`else` because there needs to\n        // be a delay between the `content()` appearing and calling `scrollTo`\n        VStack {\n            viewContent\n                .themedGroupedBackground()\n                .reloadOnAccountSwitch(entity: $post, isLoading: $isLoading) { newPost in\n                    tracker.root = .post(newPost)\n                    tracker.loadingState = .idle\n                    Task {\n                        await tracker.load(ensuringPresenceOf: scrollTargetedComment)\n                    }\n                }\n                .externalApiWarning(entity: post, isLoading: isLoading)\n                .task {\n                    await tracker.load(ensuringPresenceOf: scrollTargetedComment)\n                    if post.api == appState.firstApi {\n                        post.updateRead(true)\n                    }\n                }\n        }\n        .frame(maxWidth: .infinity, maxHeight: .infinity)\n        .conditionalNavigationTitle(post.community.value?.name ?? \"\")\n        .navigationBarTitleDisplayMode(.inline)\n        .overlay {\n            VStack {\n                if showLoadingSymbol {\n                    ZStack {\n                        palette.groupedBackground.primary\n                            .ignoresSafeArea()\n                        ProgressView()\n                            .tint(.secondary)\n                    }\n                    .transition(.opacity)\n                }\n            }\n            .animation(.easeOut(duration: 0.1), value: showLoadingSymbol)\n        }\n        .refreshable {\n            _ = await Task { @MainActor in\n                do {\n                    try await post.refresh()\n                    await tracker.refresh()\n                } catch {\n                    handleError(error)\n                }\n            }.value\n        }\n    }\n    \n    @ViewBuilder\n    var viewContent: some View {\n        GeometryReader { geo in\n            ScrollViewReader { proxy in\n                FancyScrollView {\n                    VStack(\n                        alignment: .leading,\n                        spacing: 0\n                    ) {\n                        postView(post, scrollProxy: proxy)\n                            .padding(.horizontal, Constants.main.standardSpacing)\n                        \n                        content\n                            .padding(.top, compactComments ? Constants.main.halfSpacing : Constants.main.standardSpacing)\n                        \n                        if let errorDetails = tracker.errorDetails {\n                            ErrorView(errorDetails)\n                                .frame(maxWidth: .infinity)\n                        } else if hasNoComments {\n                            noCommentsView\n                                .padding(.top, Constants.main.doubleSpacing)\n                        } else {\n                            switch tracker.loadingState {\n                            case .done:\n                                LazyVStack(spacing: 0) {\n                                    commentTree(tracker: tracker, scrollProxy: proxy)\n                                }\n                                .geometryGroup()\n                            default:\n                                ProgressView()\n                                    .tint(.themedSecondary)\n                                    .padding(.top, 50)\n                                    // This prevents the tab bar going transparent whilst the comments are loading\n                                    .padding(.bottom, 500)\n                                    .frame(maxWidth: .infinity)\n                            }\n                        }\n                    }\n                    .animation(.easeInOut(duration: 0.1), value: tracker.loadingState == .loading)\n                    .animation(.easeInOut(duration: 0.1), value: tracker.errorDetails == nil)\n                    .animation(.easeInOut(duration: 0.4), value: scrollTargetedComment?.actorId)\n                    .padding(.bottom, 80)\n                    .id(tracker.proposedDepthOffset)\n                    .transition(.opacity)\n                    .animation(.easeInOut(duration: 0.4), value: tracker.proposedDepthOffset)\n                }\n                .onChange(of: tracker.loadingState, initial: true) {\n                    if tracker.loadingState == .done, let scrollTargetedComment {\n                        // Without a slight delay here, `scrollTo` can sometimes fail. I'm not sure why this is.\n                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {\n                            proxy.scrollTo(scrollTargetedComment.actorId, anchor: .center)\n                            scrolledToScrollTargetedComment = true\n                        }\n                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                            self.scrollTargetedComment = nil\n                        }\n                    }\n                }\n                .onChange(of: jumpButtonTarget) {\n                    if let jumpButtonTarget {\n                        withAnimation {\n                            proxy.scrollTo(jumpButtonTarget, anchor: .top)\n                        }\n                        self.jumpButtonTarget = nil\n                    }\n                }\n                .overlay(alignment: jumpButton.alignment) {\n                    JumpButtonsView(\n                        showJumpButton: (tracker.nodes.count) > 1,\n                        topVisibleItem: topVisibleItem,\n                        scrollToLastVisitedPosition: showScrollToLastVisitButton(post: post) ? scrollToLastVisitedPosition : nil,\n                        scrollToNextComment: scrollToNextComment,\n                        scrollToPreviousComment: scrollToPreviousComment\n                    )\n                }\n                .onPreferenceChange(AnchorsKey.self) { updateAnchors($0, in: geo) }\n                .onAppear {\n                    if previousVisitRecord == nil {\n                        if let actorId = expandedPostHistoryTracker.retrieve(for: post.actorId) {\n                            previousVisitRecord = .revisit(topVisibleCommentAtLastVisit: actorId)\n                        } else {\n                            previousVisitRecord = .firstVisit\n                        }\n                    }\n                }\n                .toolbar { toolbarContent(post: post, scrollProxy: proxy) }\n            }\n        }\n        .environment(tracker)\n        .environment(\\.feedContext, .post)\n    }\n    \n    @ViewBuilder\n    func toolbarContent(post: Post, scrollProxy: ScrollViewProxy) -> some View {\n        sortPicker(tracker: tracker)\n        if post.shouldShowLoadingSymbol() {\n            ProgressView()\n        } else {\n            ToolbarEllipsisMenu {\n                ControlGroup {\n                    ActionButtons { _ in\n                        PostBarConfiguration.availableActions.all.compactMap { $0.createAction(post) }\n                    }\n                }\n                .controlGroupStyle(.compactMenu)\n                if !tapPostsToCollapse {\n                    Section {\n                        Button(\n                            postCollapsed ? \"Expand Post\" : \"Collapse Post\",\n                            icon: postCollapsed ? .general.expand : .general.collapse\n                        ) {\n                            togglePostCollapsed(post: post, scrollProxy: scrollProxy)\n                        }\n                    }\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func postView(_ post: Post, scrollProxy: ScrollViewProxy) -> some View {\n        Group {\n            if postCollapsed {\n                HStack {\n                    post.taggedTitle(communityContext: post.community.value)\n                        .font(.headline)\n                        .symbolVariant(.fill)\n                        .background(.themedSecondaryGroupedBackground)\n                    Spacer()\n                    Image(icon: .general.expand)\n                        .frame(height: 10)\n                }\n                .imageScale(.small)\n                .padding(Constants.main.standardSpacing)\n                .background(.themedSecondaryGroupedBackground)\n            } else {\n                LargePostView(post: post, isPostPage: true)\n            }\n        }\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n        .quickSwipes(post: post, configuration: postInteractionBar)\n        .contextMenu(post: post)\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        .onTapGesture {\n            if tapPostsToCollapse || postCollapsed {\n                togglePostCollapsed(post: post, scrollProxy: scrollProxy)\n            }\n        }\n        .id(post.actorId)\n        .transition(.opacity)\n        .anchorPreference(\n            key: AnchorsKey.self,\n            value: .center\n        ) { [post.actorId: $0] }\n    }\n}\n\nprivate struct JumpButtonsView: View {\n    @Setting(\\.comment_jumpButton) var jumpButton\n    \n    var showJumpButton: Bool\n    var topVisibleItem: TopVisibleItemContainer\n    \n    var scrollToLastVisitedPosition: (() -> Void)?\n    var scrollToNextComment: () -> Void\n    var scrollToPreviousComment: () -> Void\n    \n    var body: some View {\n        VStack(spacing: 0) {\n            if let scrollToLastVisitedPosition, topVisibleItem.isAtPost, showJumpButton {\n                JumpButtonView(\n                    icon: .lemmy.jumpToLastPositionButton,\n                    onShortPress: scrollToLastVisitedPosition,\n                    onLongPress: nil\n                )\n            }\n            if jumpButton != .none, showJumpButton {\n                JumpButtonView(\n                    onShortPress: scrollToNextComment,\n                    onLongPress: scrollToPreviousComment\n                )\n            }\n        }\n        .padding(Constants.main.standardSpacing)\n        .animation(.default, value: topVisibleItem.isAtPost)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpandedPost/MoreRepliesButton.swift",
    "content": "//\n//  MoreRepliesButton.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-11-28.\n//\n\nimport SwiftUI\n\nstruct MoreRepliesButton: View {\n    @Environment(NavigationLayer.self) var navigation\n    \n    let tracker: CommentTreeTracker\n    let commentTreeNode: CommentTreeNode\n    \n    @State var isLoading: Bool = false\n    \n    var body: some View {\n        Button {\n            isLoading = true\n            Task { @MainActor in\n                do {\n                    try await loadComments()\n                } catch {\n                    handleError(error)\n                }\n                isLoading = false\n            }\n        } label: {\n            HStack {\n                CommentBarView(depth: commentTreeNode.comment.depth + 1)\n                HStack {\n                    Text(\"More Replies\")\n                    Image(icon: .general.forward)\n                }\n                .frame(maxWidth: .infinity)\n                .padding(.vertical, 8)\n                .opacity(isLoading ? 0 : 1)\n                .overlay {\n                    if isLoading {\n                        ProgressView()\n                    }\n                }\n                .foregroundStyle(.themedAccent)\n            }\n            .background(\n                .themedSecondaryGroupedBackground,\n                in: .rect(cornerRadius: Constants.main.standardSpacing)\n            )\n            .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        }\n        .padding(.leading, CGFloat(commentTreeNode.comment.depth + 1 - tracker.proposedDepthOffset) * 10)\n        .buttonStyle(.plain)\n    }\n    \n    func loadComments() async throws {\n        let comments = try await commentTreeNode.comment.getChildren(\n            sort: tracker.sort,\n            includedParentCount: 0,\n            page: 1,\n            maxDepth: Settings.get(\\.comment_maxDepth),\n            limit: 999\n        )\n        \n        guard let maxDepth = comments.last?.depth else { return }\n        \n        // Do we want this threshold to change depending on screen size? Could be tricky if we load comments\n        // and then the user makes the window less wide (e.g. on iPad), in which case we'd need to hide\n        // the comments that exceed the new maximum width.\n        if maxDepth > 12 {\n            var comments = comments\n            if let parent = commentTreeNode.parent {\n                comments.prepend(parent.comment)\n            }\n            navigation.push(.comment(\n                commentTreeNode.comment,\n                comments: comments,\n                showViewPostButton: false))\n        } else {\n            await tracker.insertAdditionalComments(comments: comments)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpandedPost/PostStubResolutionPage.swift",
    "content": "//\n//  PostStubResolutionPage.swift\n//  Mlem\n//\n//  Created by Sjmarf on 27/09/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\n// TODO: NOW just make this an ExpectedView?\n// Or expanded post page itself take expectedValue...?\n\nstruct PostStubResolutionPage: View {\n    @Environment(NavigationLayer.self) var navigation\n    \n    let stub: PostStub\n    \n    @State var upgradeError: Error?\n    \n    var body: some View {\n        content\n            .themedGroupedBackground()\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        if let upgradeError {\n            ErrorView(.init(\n                error: upgradeError,\n                refresh: fetchPost\n            ))\n        } else {\n            ProgressView()\n                .task {\n                    await fetchPost()\n                }\n        }\n    }\n    \n    @discardableResult\n    func fetchPost() async -> Bool {\n        do {\n            let post = try await stub.getPost()\n            navigation.replace(.post(post))\n            return true\n        } catch {\n            upgradeError = error\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpectedViews/ExpectedTextView.swift",
    "content": "//\n//  ExpectedTextView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-12-19.\n//\n\nimport SwiftUI\nimport MlemMiddleware\n\nstruct ExpectedText: View {\n    let text: ExpectedValue<String>\n    private let placeholder: String\n\n    init(_ text: ExpectedValue<String>, expectedLength: Int = 15) {\n        self.text = text\n        self.placeholder = String(repeating: \"a\", count: expectedLength)\n    }\n    \n    var body: some View {\n        ZStack { // ZStack to make the animation work correctliy\n            if let text = text.value {\n                Text(text)\n                    .transition(.scale)\n            } else {\n                Text(verbatim: placeholder)\n                    .redacted(reason: .placeholder)\n                    .transition(.opacity)\n            }\n        }\n        .animation(.interactiveSpring, value: text.value != nil)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpectedViews/ExpectedView.swift",
    "content": "//\n//  ExpectedView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-12-28.\n//\n\nimport SwiftUI\nimport MlemMiddleware\n\n/// View for animating content appearance when a given ValueProviding resolves.\n/// Intended for tightly scoped, small views; may cause rendering issues on more complex views.\nstruct ExpectedView<Value, Content: View, Placeholder: View>: View {\n    let value: any ValueProviding<Value>\n    @ViewBuilder let view: (Value) -> Content\n    @ViewBuilder let placeholder: () -> Placeholder\n    \n    init(\n        _ value: any ValueProviding<Value>,\n        view: @escaping (Value) -> Content,\n        placeholder: @escaping () -> Placeholder = { EmptyView() }\n    ) {\n        self.value = value\n        self.view = view\n        self.placeholder = placeholder\n    }\n    \n    var body: some View {\n        ZStack {\n            if let value = value.value {\n                view(value)\n                    .transition(.scale)\n            } else {\n                placeholder()\n            }\n        }\n        .animation(.interactiveSpring, value: value.value == nil)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExpectedViews/String+Placeholders.swift",
    "content": "//\n//  String+Placeholders.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-01-12.\n//\n\nextension String {\n    static var personPlaceholder: Self { \"user@placeholder\" }\n    static var communityPlaceholder: Self { \"community@placeholder\" }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExportableViews/ExportableCommentEditorView.swift",
    "content": "//\n//  ExportableCommentEditorView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-11-26.\n//\n\nimport ComponentViews\nimport SwiftUI\nimport MlemMiddleware\nimport Theming\nimport os\n\nstruct ExportableCommentEditorView: View {\n    @Environment(AppState.self) var appState\n    @Environment(\\.colorScheme) var colorScheme\n    \n    @Setting(\\.appearance_palette) var palette\n    @Setting(\\.comment_createImage_showPost) var showPost: Bool\n    @Setting(\\.comment_createImage_showCreator) var showCreator: Bool\n    @Setting(\\.comment_createImage_showStats) var showStats: Bool\n    @Setting(\\.comment_createImage_colorScheme) var overrideColorScheme: UIUserInterfaceStyle\n    @Setting(\\.post_createImage_showCommunity) var postShowCommunity\n    @Setting(\\.post_createImage_showCreator) var postShowCreator\n    @Setting(\\.post_createImage_showStats) var postShowStats\n    \n    @State var commentLoader: ExportableCommentLoader\n    \n    init(comment: Comment, commentTreeTracker: CommentTreeTracker?) {\n        self.commentLoader = .init(comment: comment, tracker: commentTreeTracker)\n    }\n    \n    @State var threadLength: Int = 1 {\n        didSet {\n            guard let allParents = commentLoader.data?.comments else {\n                assertionFailure(\"Cannot modify thread length without thread\")\n                return\n            }\n            comments = allParents.suffix(threadLength)\n        }\n    }\n    @State var comments: [Comment] = .init()\n    \n    var overriddenColorScheme: ColorScheme {\n        switch overrideColorScheme {\n        case .unspecified: colorScheme\n        case .light: .light\n        case .dark: .dark\n        default: .light\n        }\n    }\n    \n    var body: some View {\n        if let error = commentLoader.error {\n            ErrorView(error)\n        } else if let data = commentLoader.data {\n            content(data: data)\n        } else {\n            ProgressView()\n                .task {\n                    await commentLoader.load()\n                }\n        }\n    }\n    \n    // swiftlint:disable:next function_body_length\n    func content(data: ExportableCommentData) -> some View {\n        ScrollView {\n            exportableComment(data: data)\n                .padding(.bottom, 200)\n        }\n        .presentationBackground(.themedGroupedBackground)\n        .overlay(alignment: .bottom) {\n            ExportableViewControlOverlay { createImageFromView(exportableComment(data: data)) }\n        }\n        .toolbar {\n            ToolbarItem(placement: .topBarLeading) {\n                CloseButtonView(ios18Label: .cancel)\n            }\n            ToolbarItem(placement: .topBarTrailing) {\n                Menu(\"Details\", icon: .general.configure) {\n                    Section(threadLength > 1 ? \"Comments\" : \"Comment\") {\n                        Toggle(\"Creator\", icon: .lemmy.person, isOn: $showCreator)\n                        Toggle(\"Stats\", icon: .lemmy.votes, isOn: $showStats)\n                    }\n                    \n                    if data.comments.count > 1 {\n                        ControlGroup(\"Parent Comments\") {\n                            Button(\"Remove Comment\", icon: .general.remove) {\n                                assert(threadLength > 1, \"Cannot decrease thread length below 1\")\n                                threadLength -= 1\n                            }\n                            .disabled(threadLength == 1)\n                            \n                            Text(verbatim: \"\\(threadLength - 1)\")\n                            \n                            Button(\"Add Comment\", icon: .general.add) {\n                                assert(\n                                    threadLength < min(8, data.comments.count),\n                                    \"Cannot increase thread length beyond \\(min(8, data.comments.count))\"\n                                )\n                                threadLength += 1\n                            }\n                            .disabled(threadLength == min(8, data.comments.count))\n                        }\n                        .labelStyle(.iconOnly)\n                        .controlGroupStyle(.compactMenu)\n                    }\n                    \n                    Section(\"Post\") {\n                        Toggle(\"Show Post\", icon: .lemmy.post, isOn: $showPost)\n                        \n                        if showPost {\n                            Toggle(\"Community\", icon: .lemmy.community, isOn: $postShowCommunity)\n                            Toggle(\"Creator\", icon: .lemmy.person, isOn: $postShowCreator)\n                            Toggle(\"Stats\", icon: .lemmy.votes, isOn: $postShowStats)\n                        }\n                    }\n                    \n                    if palette.supportedModes == .unspecified {\n                        Menu(\"Color Scheme\", icon: overrideColorScheme.icon) {\n                            Picker(\"Color Scheme\", selection: $overrideColorScheme) {\n                                ForEach(UIUserInterfaceStyle.optionCases, id: \\.self) { style in\n                                    Label(style.label, icon: style.icon)\n                                }\n                            }\n                        }\n                    }\n                }\n                .menuActionDismissBehavior(.disabled)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func exportableComment(data: ExportableCommentData) -> some View {\n        ExportableCommentView(\n            comments: data.thread(length: threadLength),\n            appState: appState,\n            colorScheme: overriddenColorScheme\n        )\n        .allowsHitTesting(false)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExportableViews/ExportableCommentLoader.swift",
    "content": "//\n//  ExportableCommentLoader.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-12-10.\n//\n\nimport SwiftUI\nimport MlemMiddleware\n\n/// Class to load and handle data required to display an exportable comment\n@Observable\nclass ExportableCommentLoader {\n    var data: ExportableCommentData?\n    var error: ErrorDetails?\n    \n    let rootComment: Comment\n    let tracker: CommentTreeTracker?\n    \n    init(comment: Comment, tracker: CommentTreeTracker?) {\n        self.rootComment = comment\n        self.tracker = tracker\n    }\n    \n    func load() async {\n        do {\n            try await rootComment.refresh()\n            var comments: [Comment]\n            if let tracker {\n                await tracker.load(ensuringPresenceOf: rootComment)\n                comments = tracker.getThread(preceding: rootComment, limit: 8)\n            } else {\n                comments = [rootComment]\n            }\n            \n            Task { @MainActor in\n                self.data = .init(comments: comments)\n            }\n        } catch {\n            self.error = handleErrorWithDetails(error)\n        }\n    }\n}\n\nstruct ExportableCommentData {\n    let comments: [Comment]\n    \n    func thread(length: Int) -> [Comment] {\n        comments.suffix(length)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExportableViews/ExportableCommentView.swift",
    "content": "//\n//  ExportableCommentView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-11-26.\n//\n\nimport SwiftUI\nimport MlemMiddleware\n\nstruct ExportableCommentView: View {\n    @Setting(\\.appearance_palette) var colorPalette\n    @Setting(\\.comment_createImage_showPost) var showPost: Bool\n    @Setting(\\.comment_createImage_showCreator) var showCreator: Bool\n    @Setting(\\.comment_createImage_showStats) var showStats: Bool\n    @Setting(\\.post_createImage_showCommunity) var postShowCommunity\n    @Setting(\\.post_createImage_showCreator) var postShowCreator\n    @Setting(\\.post_createImage_showStats) var postShowStats\n    \n    let comments: [Comment]\n    \n    // Anything environment-dependent must be passed in because ImageRenderer doesn't work with @Environment\n    let appState: AppState\n    let colorScheme: ColorScheme\n    \n    let infoStackReadouts: [CommentBarConfiguration.ReadoutType] = [.upvote, .downvote, .created, .comment]\n    \n    var showBars: Bool { showPost || comments.count > 1 }\n    \n    var animationHashValue: Int {\n        var hasher = Hasher()\n        hasher.combine(showPost)\n        hasher.combine(comments.count)\n        hasher.combine(showCreator)\n        hasher.combine(showStats)\n        hasher.combine(postShowCommunity)\n        hasher.combine(postShowCreator)\n        hasher.combine(postShowStats)\n        return hasher.finalize()\n    }\n    \n    var body: some View {\n        content\n            .background(.themedGroupedBackground)\n            .animation(.snappy, value: animationHashValue)\n            .environment(appState)\n            .palette(colorPalette.palette)\n            .environment(\\.colorScheme, colorScheme)\n    }\n    \n    var content: some View {\n        VStack(spacing: -Constants.main.standardSpacing) {\n            if showPost, let rootComment = comments.last {\n                ExpectedView(rootComment.post) { post in\n                    ExportablePostView(\n                        post: post,\n                        appState: appState,\n                        colorScheme: colorScheme\n                    )\n                    .transition(.move(edge: .top).combined(with: .opacity))\n                }\n            }\n            \n            ForEach(Array(comments.enumerated()), id: \\.element.actorId) { index, comment in\n                commentContent(comment: comment, depth: index)\n                    .geometryGroup()\n                    .padding(.leading, CGFloat(index * 10))\n                    .transition(.scale)\n            }\n        }\n    }\n    \n    func commentContent(comment: Comment, depth: Int) -> some View {\n        HStack(spacing: 0) {\n            VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n                if showCreator {\n                    ExpectedView(comment.creator) { creator in\n                        FullyQualifiedLabelView(creator, labelStyle: .medium, showFlairs: false)\n                            .transition(.scale.combined(with: .opacity))\n                    } placeholder: {\n                        Text(verbatim: .personPlaceholder).redacted(reason: .placeholder)\n                    }\n                }\n                \n                CommentBodyView(comment: comment)\n                \n                if showStats {\n                    Divider()\n                    InfoStackView(readouts: infoStackReadouts.compactMap { comment.readout(type: $0, showColor: false) })\n                        .transition(.move(edge: .top).combined(with: .scale))\n                }\n            }\n            .padding(.leading, showBars ? 11 : 0)\n            .padding(Constants.main.standardSpacing)\n        }\n        .background(.themedSecondaryGroupedBackground)\n        .clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        .overlay(alignment: .leading) {\n            // CommentBarView's maxHeight: .infinity sometimes causes scaling problems when the post is shown, putting\n            // it in an overlay forces it to respect the correct parent scaling\n            if showBars {\n                CommentBarView(depth: depth)\n                    .transition(.move(edge: .leading).combined(with: .scale))\n            }\n        }\n        .padding(Constants.main.standardSpacing)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExportableViews/ExportablePostEditorView.swift",
    "content": "//\n//  ExportablePostEditorView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-10-10.\n//\n\nimport ComponentViews\nimport Haptics\nimport Media\nimport MlemMiddleware\nimport Nuke\nimport SwiftUI\nimport Theming\n\nstruct ExportablePostEditorView: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n    \n    @Environment(\\.colorScheme) var colorScheme\n    @Setting(\\.appearance_palette) var palette\n    @Setting(\\.post_createImage_showCommunity) var showCommunity\n    @Setting(\\.post_createImage_showCreator) var showCreator\n    @Setting(\\.post_createImage_showStats) var showStats\n    @Setting(\\.post_createImage_colorScheme) var overrideColorScheme\n    \n    let post: Post\n    \n    var overriddenColorScheme: ColorScheme {\n        switch overrideColorScheme {\n        case .unspecified: colorScheme\n        case .light: .light\n        case .dark: .dark\n        default: .light\n        }\n    }\n\n    var body: some View {\n        ScrollView {\n            exportablePost\n                .padding(.bottom, 200)\n        }\n        .presentationBackground(.themedGroupedBackground)\n        .overlay(alignment: .bottom) {\n            ExportableViewControlOverlay { createImageFromView(exportablePost) }\n        }\n        .toolbar {\n            ToolbarItem(placement: .topBarLeading) {\n                CloseButtonView(ios18Label: .cancel)\n            }\n            ToolbarItem(placement: .topBarTrailing) {\n                Menu(\"Details\", icon: .general.configure) {\n                    Toggle(\"Community\", icon: .lemmy.community, isOn: $showCommunity)\n                    Toggle(\"Creator\", icon: .lemmy.person, isOn: $showCreator)\n                    Toggle(\"Stats\", icon: .lemmy.votes, isOn: $showStats)\n                    \n                    if palette.supportedModes == .unspecified {\n                        Menu(\"Color Scheme\", icon: overrideColorScheme.icon) {\n                            Picker(\"Color Scheme\", selection: $overrideColorScheme) {\n                                ForEach(UIUserInterfaceStyle.optionCases, id: \\.self) { style in\n                                    Label(style.label, icon: style.icon)\n                                }\n                            }\n                        }\n                    }\n                }\n                .menuActionDismissBehavior(.disabled)\n            }\n        }\n    }\n        \n    var exportablePost: some View {\n        ExportablePostView(\n            post: post,\n            appState: appState,\n            colorScheme: overriddenColorScheme\n        )\n        .allowsHitTesting(false)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExportableViews/ExportablePostView.swift",
    "content": "//\n//  ExportablePostView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-09-24.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ExportablePostView: View {\n    @Setting(\\.appearance_palette) var colorPalette\n    @Setting(\\.post_createImage_showCommunity) var showCommunity\n    @Setting(\\.post_createImage_showCreator) var showCreator\n    @Setting(\\.post_createImage_showStats) var showStats\n    \n    let post: Post\n    \n    // Anything environment-dependent must be passed in because ImageRenderer doesn't work with @Environment\n    let appState: AppState\n    let colorScheme: ColorScheme\n    \n    let infoStackReadouts: [PostBarConfiguration.ReadoutType] = [.upvote, .downvote, .created, .comment]\n    \n    var animationHashValue: Int {\n        var hasher = Hasher()\n        hasher.combine(showCommunity)\n        hasher.combine(showCreator)\n        hasher.combine(showStats)\n        return hasher.finalize()\n    }\n    \n    var body: some View {\n        content\n            .background(.themedSecondaryGroupedBackground)\n            .clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n            .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n            .padding(Constants.main.standardSpacing)\n            .background(.themedGroupedBackground)\n            .animation(.snappy, value: animationHashValue)\n            .environment(appState)\n            .palette(colorPalette.palette)\n            .environment(\\.colorScheme, colorScheme)\n    }\n    \n    var content: some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            if showCommunity {\n                ExpectedView(post.community) { community in\n                    FullyQualifiedLabelView(community, labelStyle: .medium, showFlairs: false)\n                        .transition(.scale.combined(with: .opacity))\n                } placeholder: {\n                    Text(verbatim: .communityPlaceholder).redacted(reason: .placeholder)\n                }\n            }\n            \n            LargePostBodyView(post: post, isPostPage: true, shouldBlur: false)\n            \n            if showCreator {\n                ExpectedView(post.creator) { creator in\n                    FullyQualifiedLabelView(creator, labelStyle: .medium, showFlairs: false)\n                        .transition(.scale.combined(with: .opacity))\n                } placeholder: {\n                    Text(verbatim: .communityPlaceholder).redacted(reason: .placeholder)\n                }\n            }\n            \n            if showStats {\n                Divider()\n                \n                InfoStackView(readouts: infoStackReadouts.compactMap { post.readout(type: $0, showColor: false) })\n                    .transition(.move(edge: .top).combined(with: .scale))\n            }\n        }\n        .padding(Constants.main.standardSpacing)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ExportableViews/ExportableViewComponents.swift",
    "content": "//\n//  ExportableViewComponents.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-11-27.\n//\n\nimport SwiftUI\n\nstruct ExportableViewControlOverlay: View {\n    // let snapshot: UIImage?\n    let createSnapshot: () -> UIImage?\n    \n    var body: some View {\n        Group {\n            if #available(iOS 26, *) {\n                content\n                    .glassEffect(.regular.interactive(), in: .capsule)\n            } else {\n                content\n                    .background(.regularMaterial, in: .capsule)\n            }\n        }\n        .padding(Constants.main.standardSpacing)\n    }\n    \n    var content: some View {\n        HStack {\n            saveButton\n            shareButton\n        }\n        .font(.title2)\n        .labelStyle(.iconOnly)\n        .buttonStyle(.plain)\n        .padding(.horizontal, Constants.main.halfSpacing)\n    }\n    \n    @ViewBuilder\n    var saveButton: some View {\n        Button(\"Save\", icon: .general.import) {\n            Task {\n                guard let imageData = createSnapshot()?.pngData() else {\n                    assertionFailure(\"Rendering failed\")\n                    ToastModel.main.add(.failure(\"Failed\"))\n                    return\n                }\n                do {\n                    try await ImageSaver().writeImageToPhotoAlbum(imageData: imageData)\n                    ToastModel.main.add(.success(\"Image Saved\"))\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n        .padding(Constants.main.standardSpacing)\n        .contentShape(.rect)\n    }\n    \n    @ViewBuilder\n    var shareButton: some View {\n        ShareLink(\n            item: TransferableUIImage(createImage: createSnapshot),\n            preview: SharePreview(\n                \"Image\",\n                image: TransferableUIImage(createImage: createSnapshot)\n            ))\n        .padding(Constants.main.standardSpacing)\n        .contentShape(.rect)\n    }\n}\n\nprivate struct TransferableUIImage: Transferable {\n    var createImage: () -> UIImage?\n    \n    enum TranferableUIImageError: Error {\n        case generationFailed\n    }\n    \n    static var transferRepresentation: some TransferRepresentation {\n        DataRepresentation(exportedContentType: .png) { item in\n            guard let imageData = item.createImage()?.pngData() else {\n                throw TranferableUIImageError.generationFailed\n            }\n            return imageData\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/FancyScrollView.swift",
    "content": "//\n//  FancyScrollView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/05/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct IsAtTopPreferenceKey: PreferenceKey {\n    static var defaultValue: Bool = true\n    static func reduce(value: inout Bool, nextValue: () -> Bool) {}\n}\n\nstruct FancyScrollView<Content: View>: View {\n    @Environment(\\.dismiss) var dismiss\n    \n    @ViewBuilder var content: () -> Content\n    @Binding var scrollToTopTrigger: Bool // TODO: investigate unifying this and isAtTop\n    var reselectAction: (() -> Void)?\n    \n    @State var isAtTop: Bool = true\n\n    private let topId: String = \"scrollToTop\"\n    \n    init(\n        scrollToTopTrigger: Binding<Bool> = .constant(false),\n        reselectAction: (() -> Void)? = nil,\n        @ViewBuilder content: @escaping () -> Content\n    ) {\n        self.content = content\n        self._scrollToTopTrigger = scrollToTopTrigger\n        self.reselectAction = reselectAction\n    }\n    \n    var body: some View {\n        ScrollViewReader { proxy in\n            ScrollView {\n                VStack(spacing: 0) {\n                    GeometryReader { geo in\n                        Color.clear.preference(\n                            key: IsAtTopPreferenceKey.self,\n                            // This must be `Int` to account for floating point error\n                            value: Int(geo.frame(in: .named(\"scrollView\")).origin.y) >= 0\n                        )\n                        .id(topId)\n                    }\n                    .frame(width: 0, height: 0)\n                    content()\n                }\n            }\n            .environment(\\.scrollProxy, proxy)\n            .onReselectTab {\n                if isAtTop {\n                    if let reselectAction {\n                        reselectAction()\n                    } else {\n                        dismiss()\n                    }\n                } else {\n                    withAnimation {\n                        proxy.scrollTo(topId)\n                    }\n                }\n            }\n            .onChange(of: scrollToTopTrigger) {\n                withAnimation {\n                    proxy.scrollTo(topId)\n                }\n            }\n            .coordinateSpace(name: \"scrollView\")\n            .onPreferenceChange(IsAtTopPreferenceKey.self) { offset in\n                if offset != isAtTop {\n                    isAtTop = offset\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/FeedFilterButtonStyle.swift",
    "content": "//\n//  FeedFilterButtonStyle.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-04.\n//\n\nimport Icons\nimport SwiftUI\n\nstruct FeedFilterButtonStyle: ButtonStyle {\n    @Environment(\\.palette) var palette\n    \n    let isOn: Bool\n    var icon: Icon? = .general.dropDown\n    \n    @ScaledMetric(relativeTo: .footnote) var height: CGFloat = 32\n    \n    var iconRequiresCircle: Bool {\n        switch icon {\n        case .general.dropDown, .general.close: true\n        default: false\n        }\n    }\n    \n    func makeBody(configuration: Configuration) -> some View {\n        HStack(spacing: 4) {\n            configuration.label\n            if let icon {\n                Image(icon: icon)\n                    .symbolRenderingMode(.hierarchical)\n                    .padding(.trailing, 8)\n                    .symbolVariant(iconRequiresCircle ? .circle.fill : .fill)\n            }\n        }\n        .frame(height: height)\n        .foregroundStyle(isOn ? .themedContrastingLabel : .themedAccent)\n        .font(.footnote)\n        .padding(icon == nil ? .horizontal : .leading, 12)\n        .background(\n            Capsule()\n                .fill(isOn ? palette.accent : .clear)\n                .strokeBorder(.themedAccent, lineWidth: isOn ? 0 : 1)\n        )\n    }\n}\n\nextension ButtonStyle where Self == FeedFilterButtonStyle {\n    @MainActor\n    static func feedFilter(\n        isOn: Bool = false,\n        icon: Icon? = .general.dropDown\n    ) -> FeedFilterButtonStyle {\n        .init(isOn: isOn, icon: icon)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/FeedToolbarOptions.swift",
    "content": "//\n//  FeedToolbarOptions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-16.\n//\n\nimport SwiftUI\n\nstruct FeedToolbarOptions: ToolbarContent {\n    @Environment(AppState.self) var appState\n    @Environment(ToastModel.self) var toastModel\n    \n    @Setting(\\.post_size) var postSize\n    @Setting(\\.feed_showRead) var showRead\n    @Setting(\\.safety_blurNsfw) var blurNsfw\n\n    var body: some ToolbarContent {\n        ToolbarItemGroup(placement: .secondaryAction) {\n            SwiftUI.Section {\n                Button(showRead ? \"Hide Read\" : \"Show Read\", icon: .settings.hideRead) {\n                    showRead.toggle()\n                    let message: LocalizedStringResource = showRead ? \"Showing Read\" : \"Hiding Read\"\n                    toastModel.add(.success(message))\n                }\n                \n                Menu {\n                    Picker(\"Post Size\", selection: $postSize) {\n                        ForEach(PostSize.allCases, id: \\.self) { item in\n                            Label(String(localized: item.label), icon: item.icon)\n                        }\n                    }\n                } label: {\n                    Label(\"Post Size\", icon: .settings.postSize)\n                }\n                \n                if appState.firstPerson?.showNsfw.value ?? false {\n                    Toggle(\n                        \"Blur NSFW\",\n                        icon: .settings.blurNsfw,\n                        isOn: .init(get: { blurNsfw != .never }, set: { blurNsfw = $0 ? .always : .never })\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/FooterLinkView.swift",
    "content": "//\n//  MarkdownFooterLinkView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-11-10.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct FooterLinkView: View {\n    let title: String\n    let subtitle: String?\n\n    var body: some View {\n        VStack(alignment: .leading, spacing: 5) {\n            Text(title)\n                .frame(maxWidth: .infinity, alignment: .leading)\n                .font(.subheadline)\n                .fontWeight(.semibold)\n                .multilineTextAlignment(.leading)\n                .lineLimit(2)\n\n            if let subtitle {\n                Text(subtitle)\n                    .font(.footnote)\n                    .lineLimit(1)\n            }\n        }\n        .foregroundStyle(.themedSecondary)\n        .padding(Constants.main.standardSpacing)\n        .background(.themedTertiaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Form/ActiveUserCountView.swift",
    "content": "//\n//  ActiveUserCountView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/08/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ActiveUserCountView: View {\n    let activeUserCount: ActiveUserCount\n    \n    var body: some View {\n        FormSection {\n            VStack(spacing: 8) {\n                Text(\"Active Users\")\n                    .foregroundStyle(.themedSecondary)\n                HStack(spacing: 16) {\n                    section(.init(month: 6), value: activeUserCount.sixMonths)\n                    section(.init(month: 1), value: activeUserCount.month)\n                    section(.init(weekOfMonth: 1), value: activeUserCount.week)\n                    section(.init(day: 1), value: activeUserCount.day)\n                }\n            }\n            .padding(.vertical, Constants.main.standardSpacing)\n        }\n    }\n    \n    @ViewBuilder\n    func section(_ components: DateComponents, value: Int) -> some View {\n        VStack {\n            Text(value.abbreviated)\n                .font(.title3)\n                .fontWeight(.semibold)\n            Text(formatter.string(from: components) ?? \"\")\n                .foregroundStyle(.themedSecondary)\n        }\n        .frame(maxWidth: .infinity)\n    }\n    \n    var formatter: DateComponentsFormatter {\n        let formatter = DateComponentsFormatter()\n        formatter.unitsStyle = .abbreviated\n        formatter.maximumUnitCount = 1\n        return formatter\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Form/CollapsibleSection.swift",
    "content": "//\n//  CollapsibleSection.swift\n//  Mlem\n//\n//  Created by Sjmarf on 02/01/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct CollapsibleSection<Content: View>: View {\n    @Environment(\\.palette) var palette\n    \n    var header: String?\n    var footer: String?\n\n    @ViewBuilder var content: () -> Content\n    @State private var collapsed: Bool\n  \n    init(\n        _ header: String? = nil,\n        footer: String? = nil,\n        collapsed: Bool = false,\n        @ViewBuilder _ content: @escaping () -> Content\n    ) {\n        self.header = header\n        self.footer = footer\n        self.content = content\n        self._collapsed = State(wrappedValue: collapsed)\n    }\n    \n    var body: some View {\n        VStack(alignment: .leading, spacing: 0) {\n            if let header {\n                HStack {\n                    Text(header)\n                        .textCase(.uppercase)\n                        .opacity(0.5)\n                    Spacer()\n                    Image(icon: .general.dropDown)\n                        .fontWeight(.semibold)\n                        .foregroundStyle(Color.accentColor)\n                        .rotationEffect(Angle(degrees: collapsed ? -90 : 0))\n                }\n                .font(.footnote)\n                .contentShape(.rect)\n                .onTapGesture { withAnimation(.default) { collapsed.toggle() }}\n                .padding(.vertical, 6)\n                .padding(.horizontal, 16)\n            }\n            \n            if !collapsed {\n                palette.groupedBackground.primary\n                    .frame(height: 1.5)\n                VStack {\n                    content()\n                }\n                \n                if let footer {\n                    Text(footer)\n                        .textCase(.uppercase)\n                        .font(.footnote)\n                        .padding(.horizontal, 16)\n                        .foregroundStyle(.themedSecondary)\n                }\n            }\n        }\n        .background(.themedSecondaryGroupedBackground)\n        .clipShape(RoundedRectangle(cornerRadius: Constants.main.mediumItemCornerRadius))\n        .fixedSize(horizontal: false, vertical: true)\n        .padding(.horizontal, 16)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Form/FormReadout.swift",
    "content": "//\n//  FormReadout.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/08/2024.\n//\n\nimport SwiftUI\n\nstruct FormReadout: View {\n    let label: LocalizedStringResource\n    let value: Int\n    \n    init(_ label: LocalizedStringResource, value: Int) {\n        self.label = label\n        self.value = value\n    }\n    \n    var body: some View {\n        FormSection {\n            VStack(spacing: Constants.main.halfSpacing) {\n                Text(label)\n                    .foregroundStyle(.themedSecondary)\n                Text(value.abbreviated)\n                    .font(.title)\n                    .fontWeight(.semibold)\n                    .foregroundStyle(.tint)\n            }\n            .padding(.vertical, Constants.main.standardSpacing)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Form/FormSection.swift",
    "content": "//\n//  FormSection.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/08/2024.\n//\n\nimport SwiftUI\n\nstruct FormSection<Content: View>: View {\n    let content: Content\n    \n    init(@ViewBuilder content: () -> Content) {\n        self.content = content()\n    }\n    \n    var body: some View {\n        content\n            .frame(maxWidth: .infinity)\n            .background(.themedSecondaryGroupedBackground)\n            .clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n            .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/HandleThreadiverseLinksModifier.swift",
    "content": "//\n//  HandleLemmyLinksModifier.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/06/2024.\n//\n\nimport MlemMiddleware\nimport SafariServices\nimport SwiftUI\n\n/// Modifier that overrides the `openURL` environment variable and attempts to open threadiverse links in-app.\nstruct HandleThreadiverseLinksModifier: ViewModifier {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(AppState.self) var appState\n    \n    @State private var showingEmailAlert = false\n    @State private var pendingMailtoURL: URL?\n    \n    // If a link in the `user@example.com` format is clicked, it opens in the Mail app\n    // immediately for these domains. For all other domains, Mlem will attempt to\n    // resolve it as a Lemmy link first.\n    private static let emailDomains: Set<String> = [\n        \"hotmail.com\",\n        \"gmail.com\",\n        \"yahoo.com\",\n        \"icloud.com\",\n        \"outlook.com\",\n        \"zoho.com\",\n        \"aol.com\",\n        \"yandex.com\",\n        \"sky.com\",\n        \"bt.com\",\n        \"btinternet.com\"\n    ]\n    \n    private static let instanceMultiplexerDomains: Set<String> = [\n        \"lemmyverse.link\",\n        \"threadiverse.link\",\n        \"vger.to\"\n    ]\n    \n    func body(content: Content) -> some View {\n        content\n            .environment(\\.openURL, OpenURLAction(handler: didReceiveURL))\n            .onChange(of: navigation.model?.pendingOpenURL) { _, url in\n                if let url {\n                    navigation.model?.pendingOpenURL = nil\n                    _ = didReceiveURL(url)\n                }\n            }\n            .alert(\"Open Mail App\", isPresented: $showingEmailAlert) {\n                Button(\"Cancel\", role: .cancel) {}\n                Button(\"Open\") {\n                    if let url = pendingMailtoURL {\n                        UIApplication.shared.open(url)\n                    }\n                }\n            } message: {\n                Text(\"Would you like to open this email address in your mail app?\")\n            }\n    }\n    \n    @MainActor\n    func didReceiveURL(_ url: URL) -> OpenURLAction.Result {\n        // TODO: Consider handling links to alternative frontends such as `old.lemmy.world` or `oldsh.itjust.works`.\n        \n        guard let scheme = url.scheme else {\n            // LemmyMarkdownUI parses the `/c/comm@example.com` and `!comm@example.com` link formats into regular links,\n            // so those don't need to be handled in this method. However, it doesn't parse links written in the format\n            // [Some text](/c/comm@example.com), which is a format that lemmy-ui supports. Those links are handled here.\n            // Later, it might be better to move that into LemmyMarkdownUI, but I think we'd need to modify the core\n            // cmark code rather than just the extensions, which isn't ideal.\n            \n            if let newUrl = createLemmyUrlFromShortcut(parts: url.pathComponents), let page = createNavigationPage(url: newUrl) {\n                navigation.push(page)\n                return .handled\n            }\n            \n            openLinkAsWebsite(url: url)\n            return .handled\n        }\n        \n        // `user@example.com` isn't recognised by Lemmy, and doesn't appear as a clickable link in lemmy-ui.\n        // The *correct* syntax is `@user@example.com`, but occasionally someone doesn't know this and\n        // types `user@example.com` instead. We handle this case by attempting to parse as a Lemmy link, and\n        // falling back to opening the Mail app if that fails.\n        if scheme == \"mailto\" {\n            if parseEmail(url: url) { return .handled }\n        }\n        \n        guard let host = url.host(), scheme.starts(with: \"http\") else {\n            openLinkAsWebsite(url: url)\n            return .handled\n        }\n        \n        if Self.instanceMultiplexerDomains.contains(host), url.pathComponents.count > 3 {\n            var components = URLComponents()\n            components.scheme = \"https\"\n            components.host = url.pathComponents[1]\n            components.path = \"/\" + url.pathComponents.dropFirst(2).joined(separator: \"/\")\n            if let newUrl = components.url, let page = createNavigationPage(url: newUrl) {\n                navigation.push(page)\n                return .handled\n            }\n        }\n        \n        // If the link is in our threadiverse domain list, push a page to the NavigationStack straight away\n        if isThreadiverseHost(host), let page = createNavigationPage(url: url) {\n            navigation.push(page)\n            return .handled\n        }\n        \n        let components = url.pathComponents.dropFirst()\n        \n        // Super-small instances may not appear in the threadiverse domain list, in which case we show a\n        // \"Loading...\" toast whilst we attempt to work out if it's actually a threadiverse link\n        if [\"u\", \"c\", \"post\", \"comment\"].contains(components.first) {\n            // The \"@\" check ensures that KBin links are excluded\n            if !host.contains(\"reddit.com\"), components.count == 2, components[1].first != \"@\" {\n                Task {\n                    await showToastAndResolve(url: url) { url in\n                        openLinkAsWebsite(url: url)\n                    }\n                }\n                return .handled\n            }\n        }\n        \n        // If all else fails, fallback to opening in browser\n        openLinkAsWebsite(url: url)\n        return .handled\n    }\n    \n    // Creates https://example.com/c/comm from /c/comm@example.com or example.com/c/comm\n    func createLemmyUrlFromShortcut(parts: [String]) -> URL? {\n        var parts = parts\n        \n        var components = URLComponents()\n        components.scheme = \"https\"\n        \n        if parts[0] != \"/\" {\n            guard parts.count == 3 else { return nil }\n            guard parts[1] == \"c\" || parts[1] == \"u\" else { return nil }\n            components.host = parts[0]\n            components.path = \"/\\(parts[1])/\\(parts[2])\"\n        } else {\n            parts.removeFirst()\n            guard parts.count == 2 else { return nil }\n            guard parts[0] == \"c\" || parts[0] == \"u\" else { return nil }\n            let fullNameParts = parts[1].split(separator: \"@\")\n            components.host = String(fullNameParts[1])\n            components.path = \"/\\(parts[0])/\\(fullNameParts[0])\"\n        }\n        return components.url\n    }\n    \n    func createNavigationPage(url: URL) -> NavigationPage? {\n        let components = Array(url.pathComponents.dropFirst())\n        if components.isEmpty, let host = url.host() {\n            return .instanceStub(InstanceStub(api: appState.firstApi, actorId: .instance(host: host)))\n        }\n        switch components.first {\n        case \"u\":\n            return .personStub(PersonStub(api: appState.firstApi, url: url))\n        case \"c\":\n            // Handle links that look like this:\n            // https://piefed.social/c/politics/p/1385905/will-the-supreme-court-hand-government-contractors-blanket-immunity\n            if components.count > 4, components[2] == \"p\" {\n                let newUrl = url.removingPathComponents().appendingPathComponent(\"post/\\(components[3])\")\n                return .postStub(PostStub(api: appState.firstApi, url: newUrl))\n            } else {\n                return .communityStub(CommunityStub(api: appState.firstApi, url: url))\n            }\n        case \"post\":\n            if let fragment = url.fragment()?.trimmingPrefix(\"comment_\") {\n                let newUrl = url.removingPathComponents().appendingPathComponent(\"comment/\\(fragment)\")\n                return .commentStub(CommentStub(api: appState.firstApi, url: newUrl))\n            } else if components.count == 2 {\n                return .postStub(PostStub(api: appState.firstApi, url: url))\n            } else if components.count == 3 {\n                let newUrl = url.removingPathComponents().appendingPathComponent(\"comment/\\(url.lastPathComponent)\")\n                return .commentStub(CommentStub(api: appState.firstApi, url: newUrl))\n            } else {\n                return nil\n            }\n        case \"comment\":\n            return .commentStub(CommentStub(api: appState.firstApi, url: url))\n        default:\n            return nil\n        }\n    }\n    \n    func parseEmail(url: URL) -> Bool {\n        let parts = url.absoluteString.trimmingPrefix(\"mailto:\").split(separator: \"@\")\n        guard parts.count == 2 else { return false }\n        let user = String(parts[0])\n        let host = String(parts[1])\n        \n        // For common email domains, show an alert asking if user wants to open mail app\n        if Self.emailDomains.contains(host) {\n            pendingMailtoURL = url\n            showingEmailAlert = true\n        } else if isThreadiverseHost(host) {\n            // If it's a Lemmy host, try to resolve as a Lemmy user\n            Task {\n                await showToastAndResolve(url: URL(string: \"https://\\(host)/u/\\(user)\")!) { _ in\n                    // If resolution fails, show email alert as fallback\n                    pendingMailtoURL = url\n                    showingEmailAlert = true\n                }\n            }\n        } else {\n            // If it's neither a common email domain nor a Lemmy host, show email alert\n            pendingMailtoURL = url\n            showingEmailAlert = true\n        }\n        \n        return true\n    }\n    \n    func showToastAndResolve(url: URL, fallback: @escaping (URL) -> Void) async {\n        let toastId = ToastModel.main.add(.loading())\n        var output: (any Sharable)?\n        do {\n            output = try await appState.firstApi.resolve(url: url)\n        } catch {\n            output = nil\n            handleError(error, silent: true)\n        }\n        if output == nil {\n            // Retry on local instance, which is needed if there is a federation boundary\n            output = try? await ApiClient.getApiClient(\n                url: url.removingPathComponents(),\n                username: nil\n            ).resolve(url: url)\n        }\n\n        if let person = output as? Person {\n            navigation.push(.person(person))\n        } else if let community = output as? Community {\n            navigation.push(.community(community))\n        } else if let post = output as? Post {\n            navigation.push(.post(post))\n        } else if let comment = output as? Comment {\n            navigation.push(.comment(comment))\n        } else {\n            fallback(url)\n        }\n        ToastModel.main.removeToast(id: toastId)\n    }\n    \n    func isThreadiverseHost(_ host: String) -> Bool {\n        MlemStats.main.hosts.contains(host)\n    }\n}\n\nfunc openLinkAsWebsite(url: URL) {\n    @Setting(\\.links_openInBrowser) var openLinksInBrowser\n    \n    if let scheme = url.scheme, scheme.hasPrefix(\"http\"), !openLinksInBrowser {\n        Task { @MainActor in\n            let viewController = SFSafariViewController(url: url, configuration: .default)\n            UIApplication.shared.firstKeyWindow?.rootViewController?.topMostViewController().present(viewController, animated: true)\n        }\n    } else {\n        UIApplication.shared.open(url)\n    }\n}\n\nprivate extension SFSafariViewController.Configuration {\n    /// The default settings used in this application\n    static var `default`: Self {\n        let configuration = Self()\n        @Setting(\\.links_readerMode) var openLinksInReaderMode\n        configuration.entersReaderIfAvailable = openLinksInReaderMode\n        return configuration\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ImageUploadMenu.swift",
    "content": "//\n//  ImageUploadMenu.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-04.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ImageUploadMenu<Label: View>: View {\n    @Environment(NavigationLayer.self) var navigation\n\n    let imageManager: ImageUploadManager\n    let imageUploadApi: ApiClient\n    @ViewBuilder let label: () -> Label\n    \n    init(imageManager: ImageUploadManager, imageUploadApi: ApiClient, @ViewBuilder label: @escaping () -> Label) {\n        self.imageManager = imageManager\n        self.imageUploadApi = imageUploadApi\n        self.label = label\n    }\n    \n    var body: some View {\n        Menu(content: {\n            Button(\"Photo Library\", icon: .general.chooseImage) {\n                navigation.showPhotosPicker(for: imageManager, api: imageUploadApi)\n            }\n            Button(\"Choose File\", icon: .general.chooseFile) {\n                navigation.showFilePicker(for: imageManager, api: imageUploadApi)\n            }\n            Button(\"Paste\", icon: .general.paste) {\n                navigation.uploadImageFromClipboard(for: imageManager, api: imageUploadApi)\n            }\n        }, label: label)\n            .disabled(imageManager.state != .idle)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Core/MediaView+Logic.swift",
    "content": "//\n//  MediaView+Helpers.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-10.\n//\n\nimport Foundation\nimport Icons\nimport SwiftUI\nimport Theming\n\nextension MediaView {\n    // MARK: Types\n    \n    enum Overlay {\n        case controls, nsfw, error\n    }\n    \n    @Observable\n    class Overlays {\n        private let overlays: Set<Overlay>\n        \n        init(_ overlays: Set<Overlay>) {\n            self.overlays = overlays\n        }\n        \n        var nsfw: Bool { overlays.contains(.nsfw) }\n        var controls: Bool { overlays.contains(.controls) }\n        var error: Bool { overlays.contains(.error) }\n    }\n\n    enum FallbackStyle {\n        case standard, avatar\n    }\n    \n    /// Enumeration of placeholder images to use if image loading fails\n    enum Fallback {\n        case personAvatar, communityAvatar, instanceAvatar, favicon, image, movie, text, link, poll, titleOnly, proxyFailure, event\n        \n        var icon: Icon {\n            switch self {\n            case .personAvatar: .lemmy.personAvatar\n            case .communityAvatar: .lemmy.communityAvatar\n            case .instanceAvatar: .lemmy.instanceAvatar\n            case .favicon: .general.browser\n            case .image: .general.missing\n            case .movie: .general.movie\n            case .text: .lemmy.textPost\n            case .link: .general.website\n            case .poll: .lemmy.pollPost\n            case .titleOnly: .lemmy.titleOnlyPost\n            case .proxyFailure: .lemmy.imageProxy\n            case .event: .lemmy.event\n            }\n        }\n        \n        /// How much of the parent view this fallback should occupy\n        var scaleFactor: CGFloat {\n            switch self {\n            case .personAvatar, .communityAvatar, .instanceAvatar, .favicon: 1.0\n            case .image, .proxyFailure: 0.375\n            case .link, .text, .poll: 0.45\n            case .titleOnly: 0.45\n            case .movie, .event: 0.6\n            }\n        }\n        \n        /// Background color for the fallback view.\n        /// - Note: this has no effect if `fallbackStyle` is `.avatar`\n        var background: ThemedColor {\n            switch self {\n            case .favicon: .clear\n            default: .themedThumbnailBackground\n            }\n        }\n        \n        var fallbackStyle: FallbackStyle {\n            switch self {\n            case .personAvatar, .communityAvatar, .instanceAvatar: .avatar\n            default: .standard\n            }\n        }\n    }\n    \n    // MARK: Functions\n    \n    func tapActions() {\n        if let onTapActions {\n            onTapActions()\n        }\n        if enableImageViewer, let navigation, let viewerUrl = fullSizeUrl ?? loader.url {\n            navigation.showImageViewer(url: viewerUrl)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Core/MediaView+Views.swift",
    "content": "//\n//  MediaView+Views.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-01-15.\n//\n\nimport Media\nimport SwiftUI\nimport Theming\n\nextension MediaView {\n    @ViewBuilder\n    var image: some View {\n        CoreMediaView(\n            media: loader.mediaType ?? .image(.blank),\n            aspectRatio: uiImage.boundedAspectRatio(bounds: aspectRatio),\n            contentMode: contentMode\n        )\n        .overlay {\n            if loader.mediaType == nil {\n                fallbackImage\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var fallbackImage: some View {\n        if loader.loading == .loading {\n            ProgressView()\n                .tint(.themedSecondary)\n        } else if !showErrorOverlay {\n            switch fallback.fallbackStyle {\n            case .standard:\n                coreFallbackImage\n                    .foregroundStyle(.themedSecondary)\n                    .background(fallback.background)\n            case .avatar:\n                coreFallbackImage\n                    .symbolRenderingMode(.palette)\n                    .foregroundStyle(.themedContrastingLabel, palette.neutralAccent.gradient)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var coreFallbackImage: some View {\n        // Use contextual fallback icons even when proxy fails.\n        let contextualFallback: Fallback = if loader.loading == .proxyFailed {\n            fallback.fallbackStyle == .avatar ? fallback : .proxyFailure\n        } else {\n            fallback\n        }\n        \n        GeometryReader { geo in\n            Image(icon: contextualFallback.icon)\n                .resizable()\n                .scaledToFit()\n                .symbolVariant(contextualFallback.fallbackStyle == .avatar ? .circle.fill : .none)\n                .frame(width: geo.size.width * contextualFallback.scaleFactor)\n                .frame(maxWidth: .infinity, maxHeight: .infinity)\n        }\n    }\n    \n    @ViewBuilder\n    var nsfwOverlay: some View {\n        if loader.loading == .done, overlays.nsfw {\n            NsfwOverlay()\n        }\n    }\n    \n    @ViewBuilder\n    var errorOverlay: some View {\n        if overlays.error,\n           let loaderError = loader.error,\n           let navigation {\n            palette.groupedBackground.tertiary.overlay {\n                switch loaderError {\n                case let .proxyFailure(proxyBypass):\n                    VStack(spacing: Constants.main.standardSpacing) {\n                        Image(icon: .lemmy.imageProxy)\n                            .resizable()\n                            .aspectRatio(contentMode: .fit)\n                            .frame(maxWidth: 50)\n                            .padding(4)\n                        \n                        Text(\"Proxy Failure\")\n                            .fontWeight(.semibold)\n                        \n                        Button(\"Load directly from \\(proxyBypass.host() ?? \"unknown host\")\") {\n                            if !bypassImageProxyShown {\n                                bypassImageProxyShown = true\n                                navigation.openSheet(.bypassImageProxyWarning {\n                                    Task {\n                                        await loader.load(proxyBypass)\n                                    }\n                                })\n                            } else {\n                                Task {\n                                    await loader.load(proxyBypass)\n                                }\n                            }\n                        }\n                        .foregroundStyle(.themedAccent)\n                        .buttonStyle(.bordered)\n                        .padding(.horizontal, Constants.main.standardSpacing)\n                    }\n                    .foregroundStyle(.themedTertiary)\n                case let .error(error):\n                    VStack {\n                        Image(icon: .general.missing)\n                            .resizable()\n                            .aspectRatio(contentMode: .fit)\n                            .frame(maxWidth: 50)\n                            .padding(4)\n                            .foregroundStyle(.themedTertiary)\n                        \n                        if let url = loader.url {\n                            Text(\"Image loading failed\")\n                                .foregroundStyle(.themedTertiary)\n                            \n                            Button(url.host() ?? String(localized: \"unknown host\"), icon: .general.browser) {\n                                openURL(url)\n                            }\n                            .tint(.themedAccent)\n                            .foregroundStyle(.themedAccent)\n                            .buttonStyle(.bordered)\n                        }\n                        \n                        if developerMode {\n                            DisclosureGroup(\"Details\") {\n                                Text(error.localizedDescription)\n                                    .foregroundStyle(.themedNegative)\n                                    .multilineTextAlignment(.center)\n                                    .padding(.top)\n                                \n                                Button(\"Copy Error\", icon: .general.copy) {\n                                    UIPasteboard.general.string = error.localizedDescription\n                                    ToastModel.main.add(.success(\"Copied\"))\n                                }\n                                .tint(.themedNegative)\n                                .foregroundStyle(.themedNegative)\n                                .buttonStyle(.bordered)\n                            }\n                            .padding(Constants.main.standardSpacing)\n                            .background(.themedBackground, in: .rect(cornerRadius: Constants.main.doubleSpacing))\n                            .padding(.horizontal, Constants.main.doubleSpacing)\n                            .padding(.top, Constants.main.standardSpacing)\n                        }\n                    }\n                    .frame(maxHeight: .infinity)\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var developerOverlay: some View {\n        if developerMode,\n           overlays.controls,\n           let ext = loader.url?.proxyAwarePathExtension?.uppercased() {\n            Text(ext)\n                .font(.footnote)\n                .fontWeight(.semibold)\n                .padding(2)\n                .padding(.horizontal, 2)\n                .background {\n                    Capsule()\n                        .fill(.regularMaterial)\n                }\n                .padding(4)\n                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)\n        }\n    }\n    \n    @ViewBuilder\n    func contextMenuContent() -> some View {\n        if let url = fullSizeUrl ?? loader.url {\n            Button(\"Save\", icon: .general.import) {\n                Task { await saveMedia(url: url) }\n            }\n            if let navigation {\n                Button(\"Share...\", icon: .general.share) {\n                    Task { await shareImage(url: url, navigation: navigation) }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Core/MediaView.swift",
    "content": "//\n//  MediaView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-01-15.\n//\n\nimport SwiftUI\nimport Media\n\nstruct MediaView: View {\n    @Environment(NavigationLayer.self) var navigation: NavigationLayer?\n    @Environment(\\.palette) var palette\n    @Environment(\\.openURL) var openURL\n    \n    @Setting(\\.status_bypassImageProxyShown) var bypassImageProxyShown\n    @Setting(\\.dev_developerMode) var developerMode\n    \n    @State var loader: MediaLoader\n    @Binding var controlState: MediaControlState\n    @State var quickLookUrl: URL?\n    \n    let url: URL?\n    \n    // appearance\n    let aspectRatio_: CoreMediaView.AspectRatioBounds?\n    var aspectRatio: CoreMediaView.AspectRatioBounds {\n        aspectRatio_ ?? .absolute(loader.mediaType?.image.validSize() ?? .init(width: 4, height: 3))\n    }\n    let contentMode: ContentMode\n    let cornerRadius: CGFloat\n    let fallback: Fallback\n    let overlays: Overlays\n    \n    // interaction\n    let enableContextMenu: Bool\n    let enableImageViewer: Bool\n    let onTapActions: (() -> Void)?\n    \n    var fullSizeUrl: URL? { Mlem.fullSizeUrl(url: loader.url) }\n    var uiImage: UIImage { loader.mediaType?.image ?? .blank }\n    var showErrorOverlay: Bool {\n        overlays.error &&\n        loader.error != nil &&\n        navigation != nil\n    }\n    var enableTap: Bool {\n        loader.loading == .done && ((onTapActions != nil) || enableImageViewer)\n    }\n\n    /// Creates a new MediaView. This view is simple by default; if no complex behaviors are specified, it will\n    /// return a plain image that fits the bounds of its parent frame.\n    /// - Parameters:\n    ///   - url: url of the media to render\n    ///   - size: target size of the media\n    ///   - controlState: MediaControlState to control this media from a parent view. If not provided, assumes inline rendering mode.\n    ///   - aspectRatioBounds: specifies the maximum vertical and horizontal aspect ratio for this image\n    ///   - contentMode: content resizing mode\n    ///   - cornerRadius: corner radius to apply to the image\n    ///   - fallback: fallback to use if image loading fails or URL is not present\n    ///   - overlays: overlays to display on the image\n    ///   - enableContextMenu: true if the default context menu (save/share/quick look) should appear\n    ///   - enableImageViewer: true if tapping the image should open the image viewer\n    ///   - onTapActions: actions to perform when the image is tapped. If `enableImageViewer: true`, tapping the image will both execute\n    ///     the specified actions and open the image viewer\n    ///  - Warning: Changing the following parameters may cause unexpected view identity changes: `enableContextMenu`, `contentMode`\n    init(\n        url: URL?,\n        size: CGSize? = nil,\n        controlState: Binding<MediaControlState>? = nil,\n        aspectRatioBounds: CoreMediaView.AspectRatioBounds? = nil,\n        contentMode: ContentMode = .fit,\n        cornerRadius: CGFloat = 0,\n        fallback: Fallback = .image,\n        overlays: Set<Overlay> = [],\n        enableContextMenu: Bool = false,\n        enableImageViewer: Bool = false,\n        onTapActions: (() -> Void)? = nil\n    ) {\n        self.url = url\n        \n        self.overlays = .init(overlays)\n        self.aspectRatio_ = aspectRatioBounds\n        self.contentMode = contentMode\n        self.cornerRadius = cornerRadius\n        self.fallback = fallback\n        \n        self.enableContextMenu = enableContextMenu\n        self.enableImageViewer = enableImageViewer\n        self.onTapActions = onTapActions\n\n        self._loader = .init(wrappedValue: .init(\n            url: url,\n            size: size,\n            autoBypassImageProxy: Settings.get(\\.privacy_autoBypassImageProxy)\n        ))\n        if let controlState {\n            self._controlState = controlState\n        } else {\n            self._controlState = .constant(.init(\n                blurred: false,\n                animating: false,\n                muted: Settings.get(\\.behavior_muteVideos)\n            ))\n        }\n        _controlState.wrappedValue.url = url\n    }\n    \n    static func largeImage(url: URL, shouldBlur: Bool, onTapActions: (() -> Void)? = nil) -> MediaView {\n        .init(\n            url: url,\n            controlState: .constant(.init(\n                blurred: shouldBlur,\n                animating: Settings.get(\\.behavior_autoplayMedia),\n                muted: Settings.get(\\.behavior_muteVideos)\n            )),\n            aspectRatioBounds: .imageDefault,\n            cornerRadius: Constants.main.mediumItemCornerRadius,\n            overlays: .init(shouldBlur ? [.controls, .nsfw, .error] : [.controls, .error]),\n            enableContextMenu: true,\n            enableImageViewer: true,\n            onTapActions: onTapActions\n        )\n    }\n    \n    var body: some View {\n        content\n            .dynamicBlur(blurred: loader.mediaType != nil && controlState.blurred)\n            .withAnimationControls()\n            .overlay(nsfwOverlay)\n            .overlay(developerOverlay)\n            .overlay(errorOverlay)\n            .clipShape(.rect(cornerRadius: cornerRadius))\n            .withContextMenu(menuContent: contextMenuContent, isEnabled: enableContextMenu && loader.error == nil)\n            .gesture(TapGesture().onEnded(tapActions), isEnabled: enableTap)\n            .frame(maxWidth: .infinity, maxHeight: .infinity)\n            .onChange(of: url, initial: true) {\n                Task {\n                    await loader.load(url)\n                }\n            }\n            .onChange(of: loader.mediaType?.isAnimated, initial: true) {\n                controlState.animationAvailable = loader.mediaType?.isAnimated ?? false\n            }\n            .environment(controlState)\n            .environment(overlays)\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        Group {\n            if #available(iOS 18.0, *) {\n                image\n                    .onScrollVisibilityChange(threshold: 0.5) { isVisible in\n                        if isVisible, controlState.autoplay {\n                            controlState.animating = true\n                        }\n                        if !isVisible {\n                            controlState.animating = false\n                        }\n                    }\n            } else {\n                image\n                    .onDisappear {\n                        controlState.animating = false\n                    }\n            }\n        }\n    }\n}\n\nprivate struct MediaViewWithContextMenu<MenuItems: View>: ViewModifier {\n    let menuContent: () -> MenuItems\n    let isEnabled: Bool\n    \n    // This sort of conditional view modifier is generally considered bad form because it can cause unexpected view identity updates.\n    // Since `enableContextMenu` is unlikely to be a dynamic value it's acceptable here; nevertheless I have put a warning\n    // in the function doc making that behavior explicit. [ Eric 2025-01-16 ]\n    func body(content: Content) -> some View {\n        if isEnabled {\n            content\n                .contextMenu {\n                    menuContent()\n                }\n        } else {\n            content\n        }\n    }\n}\n\nprivate extension View {\n    /// This view modifier ensures that the context menu is only applied if enabled. If the context menu is instead always applied\n    /// but only populated if enabled, it will disable parent context menus (e.g., in `WebsitePreviewView`).\n    func withContextMenu(menuContent: @escaping () -> some View, isEnabled: Bool) -> some View {\n        modifier(MediaViewWithContextMenu(menuContent: menuContent, isEnabled: isEnabled))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/AnimationControlLayer.swift",
    "content": "//\n//  AnimationControlLayer.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-12-06.\n//\n\nimport Media\nimport SwiftUI\n\nprivate struct AnimationControlLayer: ViewModifier {\n    @Environment(MediaControlState.self) var controlState\n    @Environment(MediaView.Overlays.self) var overlays\n    \n    // decouple controls state from blurred because the blur animation and material don't get along\n    @State var showControls: Bool = true\n    \n    func body(content: Content) -> some View {\n        if controlState.canAnimate, overlays.controls {\n            contentWithControls(content: content)\n        } else {\n            content\n        }\n    }\n    \n    @ViewBuilder\n    func contentWithControls(content: Content) -> some View {\n        content\n            .overlay {\n                if controlState.animating {\n                    Color.clear.contentShape(.rect)\n                        .highPriorityGesture(TapGesture()\n                            .onEnded {\n                                controlState.animating = false\n                            }\n                        )\n                } else if showControls {\n                    PlayButton(postSize: .large)\n                        .highPriorityGesture(TapGesture()\n                            .onEnded {\n                                controlState.animating = true\n                            }\n                        )\n                }\n            }\n            .overlay(alignment: .bottomTrailing) {\n                muteButton\n            }\n            .onChange(of: controlState.blurred, initial: true) {\n                if overlays.nsfw, overlays.controls {\n                    if controlState.blurred {\n                        showControls = false\n                    } else {\n                        controlState.animating = true\n                        showControls = true\n                    }\n                }\n            }\n    }\n    \n    @ViewBuilder\n    var muteButton: some View {\n        if controlState.audioAvailable {\n            muteButtonContent\n                .padding([.bottom, .trailing], 5)\n                .padding([.top, .leading], 15)\n                .contentShape(.rect)\n                .highPriorityGesture(TapGesture().onEnded {\n                    controlState.muted = !controlState.muted\n                })\n                .contentTransition(.symbolEffect(.replace, options: .speed(2)))\n        }\n    }\n    \n    // TODO: iOS 18 deprecation remove\n    @ViewBuilder\n    var muteButtonContent: some View {\n        if #available(iOS 26, *) {\n            muteButtonLabel\n                .glassEffect(.clear.interactive(), in: .circle)\n        } else {\n            muteButtonLabel\n                .background(.ultraThinMaterial, in: .circle)\n        }\n    }\n    \n    // TODO: iOS 18 deprecation remove\n    var muteButtonLabel: some View {\n        SmallOverlayButtonLabel(\n            isOn: controlState.muted,\n            text: (on: \"Unmute\", off: \"Mute\"),\n            icons: (on: .general.mute, off: .general.unmute))\n        .symbolVariant(.fill)\n    }\n}\n\nextension View {\n    func withAnimationControls() -> some View {\n        modifier(AnimationControlLayer())\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/NsfwOverlayView.swift",
    "content": "//\n//  NsfwOverlayView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-03.\n//\n\nimport Foundation\nimport SwiftUI\nimport Media\n\nstruct NsfwOverlay: View {\n    @Environment(MediaControlState.self) var controlState\n        \n    @MainActor\n    func setBlurred(_ newValue: Bool) {\n        withAnimation(newValue ? .easeIn(duration: 0.15) : .easeOut(duration: 0.12)) {\n            controlState.blurred = newValue\n        }\n    }\n    \n    var body: some View {\n        if controlState.blurred {\n            VStack(spacing: 8) {\n                Image(icon: .general.warning)\n                    .font(.largeTitle)\n                Text(\"NSFW\")\n                    .fontWeight(.black)\n            }\n            .foregroundStyle(.white)\n            .frame(maxWidth: .infinity, maxHeight: .infinity)\n            .contentShape(.rect)\n            .onTapGesture {\n                setBlurred(false)\n            }\n        } else {\n            Button {\n                setBlurred(true)\n            } label: {\n                blurLabel\n            }\n            .buttonStyle(.plain)\n            .padding(4)\n            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)\n        }\n    }\n    \n    // TODO: iOS 18 deprecation remove\n    @ViewBuilder\n    var blurLabel: some View {\n        if #available(iOS 26, *) {\n            blurLabelContent\n                .glassEffect(.clear.interactive(), in: .circle)\n        } else {\n            blurLabelContent\n                .background(.thinMaterial, in: .circle)\n        }\n    }\n    \n    var blurLabelContent: some View {\n        SmallOverlayButtonLabel(text: \"Blur\", icon: .general.hide)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/PlayButton.swift",
    "content": "//\n//  PlayButton.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-09-27.\n//\n\nimport SwiftUI\n\nstruct PlayButton: View {\n    let fontSize: CGFloat\n    \n    init(postSize: PostSize) {\n        self.fontSize = switch postSize {\n        case .compact, .headline: 10\n        case .tile: 20\n        case .large: 30\n        }\n    }\n    \n    var body: some View {\n        if #available(iOS 26, *) {\n            label\n                .glassEffect(.clear.interactive(), in: .circle)\n        } else {\n            label\n                .background {\n                    Circle().fill(.ultraThinMaterial)\n                }\n        }\n    }\n    \n    // TODO: iOS 18 deprecation remove\n    var label: some View {\n        Label {\n            Text(\"Play\")\n        } icon: {\n            Image(icon: .general.play)\n                .symbolVariant(.fill)\n                .font(.system(size: fontSize))\n                .foregroundStyle(.white)\n                .padding(0.6 * fontSize)\n                .contentShape(.rect)\n        }\n        .labelStyle(.iconOnly)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/SmallOverlayButtonLabel.swift",
    "content": "//\n//  SmallOverlayButtonLabel.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-11-06.\n//\n\nimport SwiftUI\nimport Icons\n\nstruct SmallOverlayButtonLabel: View {\n    let isOn: Bool\n    let text: (on: LocalizedStringResource, off: LocalizedStringResource)\n    let icons: (on: Icon, off: Icon)\n    \n    init(isOn: Bool, text: (on: LocalizedStringResource, off: LocalizedStringResource), icons: (on: Icon, off: Icon)) {\n        self.isOn = isOn\n        self.text = text\n        self.icons = icons\n    }\n    \n    init(text: LocalizedStringResource, icon: Icon) {\n        self.isOn = true\n        self.text = (on: text, off: text)\n        self.icons = (on: icon, off: icon)\n    }\n    \n    var body: some View {\n        Label {\n            Text(isOn ? text.on : text.off)\n        } icon: {\n            Image(icon: isOn ? icons.on : icons.off)\n                .resizable()\n                .scaledToFit()\n                .frame(width: 15, height: 15)\n                .padding(5)\n                .foregroundStyle(.white)\n                .contentShape(.rect)\n        }\n        .labelStyle(.iconOnly)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/ZoomRecognizer/BridgeDragValue.swift",
    "content": "//\n//  BridgeDragValue.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-31.\n//\n\nimport Foundation\nimport UIKit\n\n/// Custom struct to convert UIKit drag information to SwiftUI\nstruct BridgeDragValue {\n    let velocity: CGSize\n    let translation: CGSize\n    let startLocation: CGPoint\n    \n    init(velocity: CGSize, translation: CGSize, startLocation: CGPoint) {\n        self.velocity = velocity\n        self.translation = translation\n        self.startLocation = startLocation\n    }\n    \n    init(uiPanGesture: UIPanGestureRecognizer, startLocation: CGPoint?) {\n        assert(startLocation != nil, \"startLocation was nil\")\n        let uiVelocity = uiPanGesture.velocity(in: nil)\n        let uiTranslation = uiPanGesture.translation(in: nil)\n        self.velocity = .init(width: uiVelocity.x, height: uiVelocity.y)\n        self.translation = .init(width: uiTranslation.x, height: uiTranslation.y)\n        self.startLocation = startLocation ?? .zero\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/ZoomRecognizer/CachedComputation.swift",
    "content": "//\n//  CachedComputation.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-31.\n//\n\nclass CachedComputation<Input: Equatable, Output> {\n    private var lastInput: Input?\n    private var lastOutput: Output?\n    private var computation: (Input) -> Output\n    \n    init(computation: @escaping (Input) -> Output) {\n        self.computation = computation\n    }\n    \n    func compute(_ input: Input) -> Output {\n        if let lastInput, let lastOutput, input == lastInput {\n            return lastOutput\n        }\n        lastInput = input\n        let output = computation(input)\n        lastOutput = output\n        return output\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/ZoomRecognizer/GestureRecognizers.swift",
    "content": "//\n//  Recognizers.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-30.\n//\n\nimport SwiftUI\nimport UIKit\n\nclass MomentumResetTapGestureRecognizer: UITapGestureRecognizer {\n    var momentumKilled: Bool = false\n    var resetMomentum: () -> Bool\n    \n    init(target: Any?, action: Selector?, resetMomentum: @escaping () -> Bool) {\n        self.resetMomentum = resetMomentum\n        super.init(target: target, action: action)\n    }\n    \n    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {\n        momentumKilled = resetMomentum()\n        super.touchesBegan(touches, with: event)\n    }\n}\n\nclass PanningPinchRecognizer: UIPinchGestureRecognizer {\n    @Binding var zoomScale: CGFloat\n    var panOffset: CGSize = .zero\n    \n    init(target: Any?, action: Selector?, zoomScale: Binding<CGFloat>) {\n        _zoomScale = zoomScale\n        super.init(target: target, action: action)\n    }\n    \n    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {\n        super.touchesMoved(touches, with: event)\n        guard state == .began || state == .changed else { return }\n        let translation = translation(of: touches)\n        panOffset += translation.scaled(by: zoomScale)\n    }\n    \n    private func translation(of touches: Set<UITouch>) -> CGSize {\n        var averageLocation: CGPoint = touches.reduce(into: .zero) { result, touch in\n            result += touch.location(in: view)\n        }\n        averageLocation.x /= CGFloat(touches.count)\n        averageLocation.y /= CGFloat(touches.count)\n        \n        var previousLocation: CGPoint = touches.reduce(into: .zero) { result, touch in\n            result += touch.previousLocation(in: view)\n        }\n        previousLocation.x /= CGFloat(touches.count)\n        previousLocation.y /= CGFloat(touches.count)\n        \n        return .init(\n            width: averageLocation.x - previousLocation.x,\n            height: averageLocation.y - previousLocation.y\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/ZoomRecognizer/MomentumStatus.swift",
    "content": "//\n//  MomentumStatus.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-28.\n//\n\nimport Foundation\n\n/// Tracks the current momentum and computes position based on time\nclass MomentumStatus {\n    /// Time at which the current x momentum began\n    var xt0: CFTimeInterval?\n\n    /// Velocity when the current x momentum began\n    private var xv0: CGFloat\n    \n    /// True if x is out of bounds, false otherwise\n    private(set) var xOob: Bool = false\n    \n    /// ZoomCurve for the current x momentum\n    private var xUnitCurve: any ZoomCurve\n    \n    /// Time at which the current y momentum began\n    var yt0: CFTimeInterval?\n    \n    /// Velocity when the current y momentum began\n    private var yv0: CGFloat\n    \n    /// True if y is out of bounds, false otherwise\n    private(set) var yOob: Bool = false\n    \n    /// ZoomCurve for the current y momentum\n    private var yUnitCurve: any ZoomCurve\n    \n    init(initialVelocity: CGPoint, xOob: Bool, yOob: Bool) {\n        self.xv0 = initialVelocity.x\n        self.xOob = xOob\n        self.xUnitCurve = xOob ? PolynomialBoundReset.main : SinusoidalFriction.main\n        \n        self.yv0 = initialVelocity.y\n        self.yOob = yOob\n        self.yUnitCurve = yOob ? PolynomialBoundReset.main : SinusoidalFriction.main\n    }\n    \n    func position(at time: CFTimeInterval) -> (CGSize, active: Bool) {\n        guard let xt0, let yt0 else {\n            assertionFailure(\"Tried to query position before setting t0s\")\n            return (position: .zero, active: false)\n        }\n        \n        let (xPosition, xActive) = xUnitCurve.value(at: time - xt0)\n        let (yPosition, yActive) = yUnitCurve.value(at: time - yt0)\n        \n        return (\n            .init(width: xPosition * xv0, height: yPosition * yv0),\n            xActive || yActive\n        )\n    }\n    \n    func xLeftBounds(at time: CFTimeInterval) {\n        guard !xOob else {\n            assertionFailure(\"x left bounds twice\")\n            return\n        }\n        guard let xt0 else {\n            assertionFailure(\"x left bounds with no xt0\")\n            return\n        }\n        xOob = true\n        xv0 = xUnitCurve.velocity(at: time - xt0) * xv0\n        self.xt0 = time\n        xUnitCurve = PolynomialBoundBounce.main\n    }\n    \n    func yLeftBounds(at time: CFTimeInterval) {\n        guard !yOob else {\n            assertionFailure(\"y left bounds twice\")\n            return\n        }\n        guard let yt0 else {\n            assertionFailure(\"y left bounds with no yt0\")\n            return\n        }\n        yOob = true\n        yv0 = yUnitCurve.velocity(at: time - yt0) * yv0\n        self.yt0 = time\n        yUnitCurve = PolynomialBoundBounce.main\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/ZoomRecognizer/ZoomCurves.swift",
    "content": "//\n//  ZoomCurves.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-28.\n//\n\nimport Foundation\n\n/// This works like the native UnitCurve\nprotocol ZoomCurve {\n    func value(at progress: Double) -> (Double, active: Bool)\n    func velocity(at progress: Double) -> Double\n}\n\nclass SinusoidalFriction: ZoomCurve {\n    static var main: SinusoidalFriction { .init() }\n    \n    func value(at progress: Double) -> (Double, active: Bool) {\n        guard progress < 1 else { return (0.5, false) }\n        return (((.pi * progress) + sin(.pi * progress)) / (2 * .pi), true)\n    }\n    \n    func velocity(at progress: Double) -> Double {\n        guard progress < 1 else { return 0 }\n        return (cos(.pi * progress) + 1) / 2\n    }\n}\n\n/// ZoomCurve that starts with velocity 1, slows, then gently returns to the original position.\n/// The underlying curve equation is y = x^3 + x^2.\n/// The maximum output value is 1/3 * duration; to maintain a slope of 1 at y = 0, the curve shape is scaled by duration on both axes\nclass PolynomialBoundBounce: ZoomCurve {\n    static var main: PolynomialBoundBounce { PolynomialBoundBounce(duration: 0.3) }\n    \n    var duration: Double\n    \n    init(duration: Double = 1) {\n        self.duration = duration\n    }\n    \n    func value(at progress: Double) -> (Double, active: Bool) {\n        let scaledProgress: Double = progress / duration\n        guard scaledProgress < 1 else { return (0, false) }\n        return ((pow(scaledProgress - 1, 3) + pow(scaledProgress - 1, 2)) * duration, true)\n    }\n    \n    func velocity(at progress: Double) -> Double {\n        assertionFailure(\"Not implemented\")\n        return 0\n    }\n}\n\n/// ZoomCurve matching the shape of PolynomialBoundBounce, but starting at the furthest point of the bounce (1) and gently returning to 0.\n/// The maximum output value is 1; this curve only scales with duration along the x axis.\nclass PolynomialBoundReset: ZoomCurve {\n    static var main: PolynomialBoundReset { .init(duration: 0.25) }\n    \n    var duration: Double\n    \n    init(duration: Double = 1) {\n        self.duration = duration\n    }\n    \n    func value(at progress: Double) -> (Double, active: Bool) {\n        let scaledProgress: Double = progress / duration\n        guard scaledProgress < 1 else { return (0, false) }\n        let base: CGFloat = (2 / 3) * (scaledProgress - 1)\n        return ((pow(base, 3) + pow(base, 2)) * 6.75, true)\n    }\n    \n    func velocity(at progress: Double) -> Double {\n        assertionFailure(\"Not implemented\")\n        return 0\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/ZoomRecognizer/ZoomRecognizer.swift",
    "content": "//\n//  ZoomRecognizer.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-22.\n//\n\nimport SwiftUI\n\n// TODO: LIST\n// - Optimize\n//   - Investigate CGAffineTransform instead of scaleEffect + offset\n\nstruct ZoomRecognizer: UIViewRepresentable {\n    typealias Coordinator = ZoomRecognizerCoordinator\n\n    @Binding var scale: CGFloat\n    @Binding var offset: CGSize\n    \n    let customDragMoved: ((BridgeDragValue) -> Void)?\n    let customDragEnded: (() -> Void)?\n    let customTap: (() -> Void)?\n        \n    init(\n        scale: Binding<CGFloat>,\n        offset: Binding<CGSize>,\n        customDragMoved: ((BridgeDragValue) -> Void)? = nil,\n        customDragEnded: (() -> Void)? = nil,\n        customTap: (() -> Void)? = nil\n    ) {\n        _scale = scale\n        _offset = offset\n        self.customDragMoved = customDragMoved\n        self.customDragEnded = customDragEnded\n        self.customTap = customTap\n    }\n\n    func updateUIView(_ uiView: UIView, context: Context) {\n        // noop\n    }\n\n    func makeUIView(context: Context) -> UIView {\n        let ret: UIView = .init()\n        \n        let pinchGesture = PanningPinchRecognizer(\n            target: context.coordinator,\n            action: #selector(Coordinator.handlePinch(gesture:)),\n            zoomScale: $scale\n        )\n        pinchGesture.delegate = context.coordinator\n        ret.addGestureRecognizer(pinchGesture)\n        \n        let panGesture = UIPanGestureRecognizer(\n            target: context.coordinator,\n            action: #selector(Coordinator.handlePan(gesture:))\n        )\n        panGesture.delegate = context.coordinator\n        ret.addGestureRecognizer(panGesture)\n        \n        let doubleTap = UITapGestureRecognizer(\n            target: context.coordinator,\n            action: #selector(Coordinator.handleDoubleTap(gesture:))\n        )\n        doubleTap.numberOfTapsRequired = 2\n        doubleTap.delegate = context.coordinator\n        ret.addGestureRecognizer(doubleTap)\n        \n        let singleTap: UITapGestureRecognizer = MomentumResetTapGestureRecognizer(\n            target: context.coordinator,\n            action: #selector(Coordinator.handleSingleTap(gesture:)),\n            resetMomentum: context.coordinator.resetMomentum\n        )\n        singleTap.delegate = context.coordinator\n        ret.addGestureRecognizer(singleTap)\n        \n        return ret\n    }\n    \n    func makeCoordinator() -> Coordinator {\n        .init(\n            scale: $scale,\n            offset: $offset,\n            customDragMoved: customDragMoved,\n            customDragEnded: customDragEnded,\n            customTap: customTap\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/ZoomRecognizer/ZoomRecognizerCoordinator+GestureRecognition.swift",
    "content": "//\n//  ZoomRecognizerCoordinator+GestureRecognition.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-31.\n//\n\nimport UIKit\n\nextension ZoomRecognizerCoordinator {\n    func gestureRecognizer(\n        _ gestureRecognizer: UIGestureRecognizer,\n        shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer\n    ) -> Bool {\n        if let doubleTap = gestureRecognizer as? UITapGestureRecognizer {\n            if doubleTap.numberOfTapsRequired == 2, otherGestureRecognizer is UIPanGestureRecognizer {\n                // prevents quick pan gestures from triggering as double tap\n                return false\n            }\n            return true\n        }\n        return false\n    }\n    \n    func gestureRecognizer(\n        _ gestureRecognizer: UIGestureRecognizer,\n        shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer\n    ) -> Bool {\n        // single tap should require double tap to fail unless it killed momentum\n        if let momentumResetGesture = gestureRecognizer as? MomentumResetTapGestureRecognizer,\n           !momentumResetGesture.momentumKilled,\n           let doubleTap = otherGestureRecognizer as? UITapGestureRecognizer,\n           doubleTap.numberOfTapsRequired == 2 {\n            return true\n        }\n        return false\n    }\n    \n    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {\n        if gestureRecognizer is UIPinchGestureRecognizer || gestureRecognizer is UITapGestureRecognizer {\n            return true\n        } else if gestureRecognizer is UIPanGestureRecognizer {\n            let location = gestureRecognizer.location(in: nil)\n            if gestureRecognizer.numberOfTouches == 1 {\n                if zoomSliderLocation.leftEnabled && leftZoomSliderHitbox.contains(location) ||\n                    zoomSliderLocation.rightEnabled && rightZoomSliderHitbox.contains(location) {\n                    panType = .zoom\n                    return true\n                } else if scale > 1.0 {\n                    panType = .move\n                    return true\n                } else {\n                    panType = .custom\n                    return true\n                }\n            }\n            return false\n        } else {\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/ZoomRecognizer/ZoomRecognizerCoordinator+Logic.swift",
    "content": "//\n//  ZoomRecognizerCoordinator+Logic.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-31.\n//\n\nimport SwiftUI\nimport UIKit\n\nextension ZoomRecognizerCoordinator {\n    // MARK: - Pan handlers\n    \n    /// Reacts to the given pan gesture by updating offset according to the gesture's translation. When the gesture ends,\n    /// if it in bounds on any axis and moving faster than the momentum threshold (40), the view will continue to pan\n    /// with momentum; otherwise it will stop and, if needed, reset to bounds.\n    func handleMovePan(gesture: UIPanGestureRecognizer) {\n        switch gesture.state {\n        case .possible:\n            break\n        case .began:\n            initializeBounds(view: gesture.view)\n            resetMomentum()\n            initialOffset = offset\n            \n            updateOffsetForPanGesture(gesture)\n        case .changed:\n            updateOffsetForPanGesture(gesture)\n        case .ended, .cancelled:\n            panType = .none\n            guard let view = gesture.view else {\n                assertionFailure(\"Missing view or bounds\")\n                return\n            }\n            \n            initialScale = scale\n            \n            let gestureVelocity = gesture.velocity(in: view)\n            let maxOffsets = maxOffsets.compute(scale)\n            let xOob = abs(offset.width) >= maxOffsets.width\n            let yOob = abs(offset.height) >= maxOffsets.height\n            if !(xOob && yOob),\n               abs(gestureVelocity.x) + abs(gestureVelocity.y) > 40 {\n                startMomentum(\n                    velocity: gestureVelocity,\n                    xOob: xOob,\n                    yOob: yOob,\n                    maxXOffset: maxOffsets.width,\n                    maxYOffset: maxOffsets.height\n                )\n            } else {\n                let translation = gesture.translation(in: view)\n                resetToBounds(activeOffset: .init(width: translation.x, height: translation.y).scaled(by: scale))\n            }\n        case .failed:\n            panType = .none\n        default:\n            assertionFailure(\"Unknown state\")\n        }\n    }\n    \n    /// Reacts to the given pan gesture by updating zoom according to the height of the pan gesture\n    func handleZoomPan(gesture: UIPanGestureRecognizer) {\n        switch gesture.state {\n        case .possible:\n            break\n        case .began:\n            initializeBounds(view: gesture.view)\n            guard let bounds else {\n                assertionFailure(\"No bounds\")\n                return\n            }\n            resetMomentum()\n            initialScale = scale\n            initialOffset = offset\n            let xAnchor = (((scale * bounds.width) / 2) - offset.width) / (scale * bounds.width)\n            let yAnchor = (((scale * bounds.height) / 2) - offset.height) / (scale * bounds.height)\n            anchor = .init(x: xAnchor, y: yAnchor)\n        case .changed:\n            let newScale = (initialScale + (gesture.translation(in: nil).y / -60)).bounded(lower: 1.0, upper: 4.0)\n            let maxOffsets = maxOffsets.compute(newScale)\n            let offsetDeltas = computeOffsetDeltas(scaleFactor: newScale / initialScale)\n            let newOffset = initialOffset + offsetDeltas\n            \n            scale = newScale\n            offset = .init(\n                width: newOffset.width.bounded(lower: -maxOffsets.width, upper: maxOffsets.width),\n                height: newOffset.height.bounded(lower: -maxOffsets.height, upper: maxOffsets.height)\n            )\n        case .ended, .cancelled, .failed:\n            panType = .none\n        default:\n            assertionFailure(\"Unknown state\")\n        }\n    }\n\n    /// Reacts to the given pan gesture using user-provided drag callbacks\n    func handleCustomPan(gesture: UIPanGestureRecognizer) {\n        switch gesture.state {\n        case .possible:\n            break\n        case .began:\n            customPanStartLocation = gesture.location(in: nil)\n            customDragMoved?(.init(uiPanGesture: gesture, startLocation: customPanStartLocation))\n        case .changed:\n            customDragMoved?(.init(uiPanGesture: gesture, startLocation: customPanStartLocation))\n        case .ended, .cancelled:\n            customPanStartLocation = nil\n            if let customDragEnded {\n                customDragEnded()\n            }\n        case .failed:\n            customPanStartLocation = nil\n        default:\n            assertionFailure(\"Unrecognized state\")\n        }\n    }\n    \n    /// Updates offset according to the translation of the given pan gesture recognizer\n    func updateOffsetForPanGesture(_ gesture: UIPanGestureRecognizer) {\n        guard let view = gesture.view else {\n            assertionFailure(\"No view\")\n            return\n        }\n        \n        let translation = gesture.translation(in: view)\n        offset = initialOffset + .init(width: translation.x, height: translation.y).scaled(by: scale)\n    }\n    \n    // MARK: - Pinch handlers\n    \n    /// Prepares the view to pinch on a given point\n    func beginPinch(at location: CGPoint) {\n        guard let bounds else {\n            assertionFailure(\"No bounds\")\n            return\n        }\n        \n        initialScale = scale\n        initialOffset = offset\n        anchor = .init(x: location.x / bounds.width, y: location.y / bounds.height)\n    }\n    \n    /// Updates the view based on the given scale and pan offset such that the anchor remains centered on the pinch\n    func updatePinch(with scale: CGFloat, panOffset: CGSize) {\n        let targetZoomScale: CGFloat = (initialScale * scale).softBounded(softMin: 1, hardMin: 0.6, softMax: 4, hardMax: 6)\n        let adjustedScale: CGFloat = targetZoomScale / initialScale\n        \n        self.scale = targetZoomScale\n        let offsetDeltas = computeOffsetDeltas(scaleFactor: adjustedScale)\n        offset = initialOffset + panOffset + offsetDeltas\n    }\n    \n    func endPinch(gesture: PanningPinchRecognizer) {\n        resetToBounds(activeOffset: gesture.panOffset)\n        gesture.panOffset = .zero\n    }\n    \n    // MARK: - Momentum\n    \n    func startMomentum(velocity: CGPoint, xOob: Bool, yOob: Bool, maxXOffset: CGFloat, maxYOffset: CGFloat) {\n        initialScale = scale\n        initialOffset = offset\n        \n        let xVelo: CGFloat\n        if xOob {\n            let xBound: CGFloat = offset.width < 0 ? -maxXOffset : maxXOffset\n            initialOffset.width = xBound\n            xVelo = offset.width - xBound\n        } else {\n            xVelo = velocity.x * scale\n        }\n        \n        let yVelo: CGFloat\n        if yOob {\n            let yBound: CGFloat = offset.height < 0 ? -maxYOffset : maxYOffset\n            initialOffset.height = yBound\n            yVelo = offset.height - yBound\n        } else {\n            yVelo = velocity.y * scale\n        }\n        \n        momentum = .init(\n            initialVelocity: .init(x: xVelo, y: yVelo),\n            xOob: xOob,\n            yOob: yOob\n        )\n        \n        // TODO: optimize this to use the full 120fps available on ProMotion displays\n        let link = CADisplayLink(target: self, selector: #selector(tickMomentum))\n        link.preferredFrameRateRange = .init(minimum: 70, maximum: 90, __preferred: 90)\n        link.add(to: .current, forMode: .default)\n        self.link = link\n    }\n    \n    @objc\n    func tickMomentum(displayLink: CADisplayLink) {\n        guard let momentum else {\n            assertionFailure(\"Timer fired with no momentum\")\n            return\n        }\n        \n        // set up initial times\n        if momentum.xt0 == nil {\n            momentum.xt0 = displayLink.timestamp\n        }\n        if momentum.yt0 == nil {\n            momentum.yt0 = displayLink.timestamp\n        }\n        \n        // check out-of-bounds\n        let maxOffsets = maxOffsets.compute(scale)\n        if !momentum.xOob, abs(offset.width) >= maxOffsets.width {\n            initialOffset.width = maxOffsets.width * (offset.width < 0 ? -1 : 1)\n            momentum.xLeftBounds(at: displayLink.timestamp)\n        }\n        if !momentum.yOob, abs(offset.height) >= maxOffsets.height {\n            initialOffset.height = maxOffsets.height * (offset.height < 0 ? -1 : 1)\n            momentum.yLeftBounds(at: displayLink.timestamp)\n        }\n        \n        // compute offset\n        let (increment, active) = momentum.position(at: displayLink.targetTimestamp)\n        \n        offset = initialOffset + increment\n        if !active { resetMomentum() }\n    }\n    \n    /// Halts momentum physics\n    /// - Returns: true if momentum was killed, false if noop (no momentum when called)\n    @discardableResult\n    @objc\n    func resetMomentum() -> Bool {\n        let ret = momentum != nil\n        link?.invalidate()\n        link = nil\n        momentum = nil\n        return ret\n    }\n    \n    // MARK: - Zoom\n    \n    /// Computes the difference that needs to be applied to the offset to anchor the zoom effect at `anchor` for\n    /// a given `scaleFactor`, where `scaleFactor` is the ratio of the target scale to the scale when `anchor` was set.\n    func computeOffsetDeltas(scaleFactor: CGFloat) -> CGSize {\n        guard let bounds else {\n            assertionFailure(\"No bounds\")\n            return .zero\n        }\n        let scaledBounds: CGSize = .init(width: bounds.width, height: bounds.height).scaled(by: initialScale)\n        \n        // (scale - 1) * (0.5 - anchor) computes the offset required to center the view on the anchor while zooming,\n        // expressed in a percentage of the zoomed view's bounds; * scaledBounds.width transforms that into an\n        // offset in real px\n        let xOffset: CGFloat = (scaleFactor - 1) * (0.5 - anchor.x) * scaledBounds.width\n        let yOffset: CGFloat = (scaleFactor - 1) * (0.5 - anchor.y) * scaledBounds.height\n        \n        return .init(width: xOffset, height: yOffset)\n    }\n    \n    // MARK: - Bounds\n    \n    /// If bounds are not set, initializes them using the given UIView. The view is declared as optional to make this function\n    /// easy to call, but is expected to be defined.\n    func initializeBounds(view: UIView?) {\n        guard let view else {\n            assertionFailure(\"No view\")\n            return\n        }\n        \n        if bounds == nil, view.bounds != .zero {\n            bounds = .init(width: view.bounds.width, height: view.bounds.height)\n        }\n    }\n    \n    func isOutOfBounds(offset: CGSize) -> Bool {\n        guard let bounds else {\n            assertionFailure(\"No bounds\")\n            return false\n        }\n        return abs(offset.width) > bounds.width || abs(offset.height) > bounds.height\n    }\n    \n    /// Resets offset and scale to be within bounds\n    func resetToBounds(activeOffset: CGSize) {\n        guard let bounds else {\n            assertionFailure(\"No bounds\")\n            return\n        }\n        \n        let boundedScale: CGFloat = scale.bounded(lower: 1.0, upper: 4.0)\n        let offsetDeltas = computeOffsetDeltas(scaleFactor: boundedScale / initialScale) + activeOffset\n        let maxOffsets = maxOffsets.compute(boundedScale)\n        \n        let newOffset: CGSize = .init(\n            width: (initialOffset.width + offsetDeltas.width).bounded(lower: -maxOffsets.width, upper: maxOffsets.width),\n            height: (initialOffset.height + offsetDeltas.height).bounded(lower: -maxOffsets.height, upper: maxOffsets.height)\n        )\n        \n        withAnimation(.easeOut(duration: 0.25)) {\n            offset = newOffset\n            scale = boundedScale\n        }\n        \n        initialOffset = newOffset\n        anchor = .init(x: newOffset.width / bounds.width, y: newOffset.height / bounds.height)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Helpers/ZoomRecognizer/ZoomRecognizerCoordinator.swift",
    "content": "//\n//  ZoomRecognizerCoordinator.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-30.\n//\n\nimport os\nimport SwiftUI\nimport UIKit\n\nenum PanType {\n    case move, zoom, custom, none\n}\n\nclass ZoomRecognizerCoordinator: NSObject, UIGestureRecognizerDelegate {\n    private let log: Logger = .mlemLogger()\n    \n    @Setting(\\.a11y_zoomSliderLocation) var zoomSliderLocation\n    \n    @Binding var scale: CGFloat\n    @Binding var offset: CGSize\n    \n    let customDragMoved: ((BridgeDragValue) -> Void)?\n    let customDragEnded: (() -> Void)?\n    let customTap: (() -> Void)?\n    \n    /// Scale when the current gesture began\n    var initialScale: CGFloat = 1.0\n    \n    /// Offset when the current gesture began\n    var initialOffset: CGSize = .zero\n    \n    /// Point in the image where the zoom gesture is anchored\n    var anchor: UnitPoint = .center\n    \n    var link: CADisplayLink?\n    var momentum: MomentumStatus?\n    \n    /// Bounds of the view\n    var bounds: CGSize?\n    \n    var panType: PanType = .none\n    \n    var customPanStartLocation: CGPoint?\n    \n    /// Computes the maximum allowed offsets for a given scale.\n    /// - Note: to get the minimum offset, multiply the return value by -1.\n    lazy var maxOffsets: CachedComputation<CGFloat, CGSize> = .init { input in\n        guard let bounds = self.bounds else {\n            assertionFailure(\"No bounds\")\n            return .zero\n        }\n        return bounds.scaled(by: (input - 1) / 2)\n    }\n    \n    let leftZoomSliderHitbox: CGRect = .init(\n        origin: .init(x: 0, y: 70),\n        size: .init(width: 40, height: UIScreen.main.bounds.height - 140)\n    )\n    let rightZoomSliderHitbox: CGRect = .init(\n        origin: .init(x: UIScreen.main.bounds.width - 40, y: 70),\n        size: .init(width: 40, height: UIScreen.main.bounds.height - 140)\n    )\n    \n    init(\n        scale: Binding<CGFloat>,\n        offset: Binding<CGSize>,\n        customDragMoved: ((BridgeDragValue) -> Void)? = nil,\n        customDragEnded: (() -> Void)? = nil,\n        customTap: (() -> Void)? = nil\n    ) {\n        _scale = scale\n        _offset = offset\n        self.customDragMoved = customDragMoved\n        self.customDragEnded = customDragEnded\n        self.customTap = customTap\n    }\n    \n    @objc\n    func handlePinch(gesture: PanningPinchRecognizer) {\n        switch gesture.state {\n        case .possible:\n            break\n        case .began:\n            guard let view = gesture.view else {\n                assertionFailure(\"No view\")\n                return\n            }\n            initializeBounds(view: view)\n            resetMomentum()\n            beginPinch(at: gesture.location(in: view))\n        case .changed:\n            updatePinch(with: gesture.scale, panOffset: gesture.panOffset)\n        case .ended, .cancelled:\n            endPinch(gesture: gesture)\n        case .failed:\n            log.debug(\"Pinch gesture failed\")\n        default:\n            assertionFailure(\"Unknown state\")\n        }\n    }\n    \n    @objc\n    func handlePan(gesture: UIPanGestureRecognizer) {\n        switch panType {\n        case .move:\n            handleMovePan(gesture: gesture)\n        case .zoom:\n            handleZoomPan(gesture: gesture)\n        case .custom:\n            handleCustomPan(gesture: gesture)\n        case .none:\n            assertionFailure(\"Pan started with no valid pan type\")\n        }\n    }\n    \n    @objc\n    func handleDoubleTap(gesture: UITapGestureRecognizer) {\n        guard let view = gesture.view else {\n            assertionFailure(\"No view\")\n            return\n        }\n        initializeBounds(view: view)\n        \n        guard let bounds else {\n            assertionFailure(\"No bounds\")\n            return\n        }\n        \n        initialOffset = offset\n        initialScale = scale\n        \n        let targetZoomScale: CGFloat\n        let newOffset: CGSize\n        if scale == 1 {\n            let location = gesture.location(in: view)\n            targetZoomScale = 3\n            anchor = .init(x: location.x / bounds.width, y: location.y / bounds.height)\n            let offsetDeltas = computeOffsetDeltas(scaleFactor: targetZoomScale / initialScale)\n            let maxOffsets = maxOffsets.compute(targetZoomScale)\n            \n            newOffset = .init(\n                width: (initialOffset.width + offsetDeltas.width).bounded(lower: -maxOffsets.width, upper: maxOffsets.width),\n                height: (initialOffset.height + offsetDeltas.height).bounded(lower: -maxOffsets.height, upper: maxOffsets.height)\n            )\n        } else {\n            targetZoomScale = 1\n            anchor = .center\n            newOffset = .zero\n        }\n        \n        withAnimation(.easeInOut(duration: 0.25)) {\n            offset = newOffset\n            scale = targetZoomScale\n        }\n    }\n    \n    @objc\n    func handleSingleTap(gesture: MomentumResetTapGestureRecognizer) {\n        initializeBounds(view: gesture.view)\n\n        let maxOffsets = maxOffsets.compute(scale)\n        if abs(offset.width) > maxOffsets.width || abs(offset.height) > maxOffsets.height {\n            resetToBounds(activeOffset: offset - initialOffset)\n        }\n        \n        if gesture.momentumKilled {\n            gesture.momentumKilled = false\n        } else if let customTap {\n            customTap()\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Wrappers/CircleCroppedImageView.swift",
    "content": "//\n//  CircleCroppedImageView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-02.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\nimport Media\n\n/// Convenience struct to automatically circle-crop an image. Also applies the given `frame` parameter as a frame to the view.\nstruct CircleCroppedImageView: View {\n    let url: URL?\n    let frame: CGFloat // only need one CGFloat because always 1:1 aspect ratio\n    let fallback: MediaView.Fallback\n    let showProgress: Bool\n    let blurred: Bool\n    let enableAnimation: Bool\n    \n    /// Creates an image from the given URL cropped into a circle\n    /// - Parameters:\n    ///   - url: URL of the image to render\n    ///   - frame: frame to crop the image into\n    ///   - fallback: fallback image\n    ///   - showProgress: true if the progress spinner should be displayed, false otherwise. Defaults to true.\n    ///   - blurred: true if the image should be blurred, false otherwise. Defaults to false.\n    ///   - enableAnimation: true if the image should animate, false if it should not.\n    ///     If unspecified, will only animate if the animated avatars settings is `.always`\n    init(\n        url: URL?,\n        frame: CGFloat,\n        fallback: MediaView.Fallback,\n        showProgress: Bool = true,\n        blurred: Bool = false,\n        enableAnimation: Bool = (Settings.get(\\.media_animatedAvatars) == .always)\n    ) {\n        self.url = url\n        self.frame = frame\n        self.fallback = fallback\n        self.showProgress = showProgress\n        self.blurred = blurred\n        self.enableAnimation = enableAnimation\n    }\n    \n    var body: some View {\n        MediaView(\n            url: url,\n            size: .init(width: frame, height: frame),\n            controlState: .constant(.init(\n                blurred: blurred,\n                animating: enableAnimation,\n                muted: Settings.get(\\.behavior_muteVideos)\n            )),\n            aspectRatioBounds: .absoluteSquare,\n            contentMode: .fill,\n            fallback: fallback\n        )\n        .clipShape(Circle())\n        .geometryGroup()\n        .frame(width: frame, height: frame)\n    }\n}\n\n// convenience initializers for avatars\nextension CircleCroppedImageView {\n    init<T: ProfileProviding>(\n        _ model: T?,\n        frame: CGFloat,\n        blurred: Bool = false,\n        showProgress: Bool = true\n    ) {\n        self.init(\n            url: model?.avatar,\n            frame: frame,\n            fallback: T.avatarFallback,\n            blurred: blurred\n        )\n    }\n\n    init(\n        _ model: any ProfileProviding,\n        frame: CGFloat,\n        blurred: Bool = false,\n        showProgress: Bool = true\n    ) {\n        self.init(\n            url: model.avatar,\n            frame: frame,\n            fallback: Swift.type(of: model).avatarFallback,\n            blurred: blurred\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Wrappers/SimpleAvatarView.swift",
    "content": "//\n//  SimpleAvatarView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/07/2024.\n//\n\nimport MlemMiddleware\nimport Nuke\nimport Rest\nimport SwiftUI\n\nstruct SimpleAvatarView: View {\n    @State private var uiImage: UIImage\n    @State private var loading: Bool\n\n    let url: URL?\n    let type: MediaView.Fallback\n\n    init(\n        url: URL?,\n        type: MediaView.Fallback\n    ) {\n        self.url = url\n        self.type = type\n\n        self._uiImage = .init(wrappedValue: .init())\n        self._loading = .init(wrappedValue: url != nil)\n    }\n\n    var defaultImage: UIImage {\n        guard let fromIcon: UIImage = .init(icon: type.icon) else {\n            assertionFailure(\"Could not create default image from \\(type.icon)\")\n            return .blank\n        }\n        return fromIcon\n            .applyingSymbolConfiguration(.init(\n                font: .systemFont(ofSize: 17),\n                scale: .large\n            ))!\n            .withTintColor(.secondaryLabel, renderingMode: .alwaysOriginal)\n    }\n\n    var body: some View {\n        Group {\n            if url == nil {\n                Image(uiImage: defaultImage)\n                    .symbolVariant(.circle.fill)\n            } else {\n                Image(uiImage: uiImage)\n                    .task { await loadImage() }\n            }\n        }\n    }\n\n    func loadImage() async {\n        guard let url else { return }\n\n        do {\n            let urlRequest = mlemUrlRequest(url: url)\n            let imageTask = ImagePipeline.shared.imageTask(with: .init(urlRequest: urlRequest))\n\n            let image = try await imageTask.image\n            uiImage = image.circleMasked\n            loading = false\n        } catch {\n            handleError(error, silent: true)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Wrappers/ThumbnailImageView.swift",
    "content": "//\n//  ThumbnailImageView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-19.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport QuickLook\nimport SwiftUI\nimport Media\n\nstruct ThumbnailImageView: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.openURL) var openURL\n    \n    @Setting(\\.a11y_websiteThumbnailIcon) var websiteThumbnailIcon\n    @Setting(\\.post_size) var postSize\n    \n    @State var mediaControlState: MediaControlState\n    @State var quickLookUrl: URL?\n    \n    let post: Post\n    let size: Size\n    let frame: CGSize\n    \n    enum Size {\n        case standard, tile\n    }\n    \n    var url: URL? {\n        switch post.type {\n        case let .media(url), let .embedded(url, _): url\n        case let .link(link): link.thumbnail\n        default: nil\n        }\n    }\n    \n    var onTapActions: (() -> Void)? {\n        switch post.type {\n        case .media, .embedded:\n            { post.updateRead(true) }\n        case let .link(link):\n            {\n                post.updateRead(true)\n                openURL(link.content)\n            }\n        default:\n            nil\n        }\n    }\n    \n    init(\n        post: Post,\n        blurred: Bool,\n        size: Size,\n        frame: CGSize\n    ) {\n        self.post = post\n        self.size = size\n        self.frame = frame\n        \n        self._mediaControlState = .init(wrappedValue: .init(\n            blurred: blurred,\n            animating: false,\n            enableAnimation: false,\n            muted: Settings.get(\\.behavior_muteVideos)\n        ))\n    }\n    \n    var body: some View {\n        content\n            .overlay {\n                if websiteThumbnailIcon, case .link = post.type {\n                    Image(icon: .general.browser)\n                        .frame(width: 16, height: 16)\n                        .foregroundStyle(.white)\n                        .background(.ultraThinMaterial, in: .circle)\n                        .padding(4)\n                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)\n                }\n            }\n            .frame(width: frame.width, height: frame.width)\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        MediaView(\n            url: url,\n            size: frame,\n            controlState: $mediaControlState,\n            aspectRatioBounds: .absoluteSquare,\n            contentMode: .fill,\n            cornerRadius: size == .tile ? 0 : Constants.main.smallItemCornerRadius,\n            fallback: post.imageFallback,\n            enableContextMenu: post.type.isMedia,\n            enableImageViewer: post.type.isMedia,\n            onTapActions: onTapActions\n        )\n        .overlay {\n            if mediaControlState.animationAvailable {\n                PlayButton(postSize: postSize)\n            }\n        }\n    }\n    \n    func shareImage(url: URL) async {\n        if let fileUrl = await downloadImageToFileSystem(url: url) {\n            navigation.model?.shareInfo = .init(url: fileUrl)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Images/Wrappers/ZoomableImageView.swift",
    "content": "//\n//  ZoomableImageView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-31.\n//\n\nimport SwiftUI\nimport Media\n\nstruct ZoomableImageView: View {\n    let url: URL\n    @Binding var controlState: MediaControlState\n    @Binding var scale: CGFloat\n    @Binding var offset: CGSize\n    let customDragMoved: ((BridgeDragValue) -> Void)?\n    let customDragEnded: (() -> Void)?\n    let customTap: (() -> Void)?\n    \n    var body: some View {\n        MediaView(url: url, controlState: $controlState, overlays: .init([.error]))\n            .overlay {\n                ZoomRecognizer(\n                    scale: $scale,\n                    offset: $offset,\n                    customDragMoved: customDragMoved,\n                    customDragEnded: customDragEnded,\n                    customTap: customTap\n                )\n            }\n            .scaleEffect(scale)\n            .offset(x: offset.width, y: offset.height)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/InfoStackView.swift",
    "content": "//\n//  InfoStackView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/06/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct InfoStackView: View {\n    let readouts: [Readout]\n    \n    var body: some View {\n        HStack(spacing: 12) {\n            ForEach(readouts, id: \\.viewId) { readout in\n                ReadoutView(readout: readout)\n            }\n        }\n        .geometryGroup()\n    }\n}\n\nstruct ReadoutView: View {\n    @Environment(\\.palette) var palette\n    \n    let readout: Readout\n    \n    var body: some View {\n        HStack(spacing: 2) {\n            Image(systemName: readout.icon)\n            Group {\n                if readout.label?.allSatisfy(\\.isNumber) ?? false {\n                    Text(readout.label ?? \" \")\n                        .monospacedDigit()\n                } else {\n                    Text(readout.label ?? \" \")\n                }\n            }\n            .contentTransition(.numericText(value: Double(readout.label ?? \"\") ?? 0))\n            .animation(.default, value: readout.label)\n            if let value = readout.value {\n                Text(value)\n                    .monospacedDigit()\n                    .foregroundStyle(readout.valueColor ?? .themedSecondary)\n            }\n        }\n        .foregroundStyle(readout.color ?? .themedSecondary)\n        .font(.footnote)\n        .lineLimit(1)\n    }\n}\n\nextension InfoStackView {\n    init(post: Post, readouts: [PostBarConfiguration.ReadoutType], coloredReadouts: Set<PostBarConfiguration.ReadoutType>) {\n        self.readouts = readouts.compactMap { post.readout(type: $0, showColor: coloredReadouts.contains($0)) }\n    }\n    \n    init(\n        comment: Comment,\n        readouts: [CommentBarConfiguration.ReadoutType],\n        coloredReadouts: Set<CommentBarConfiguration.ReadoutType>\n    ) {\n        self.readouts = readouts.compactMap { comment.readout(type: $0, showColor: coloredReadouts.contains($0)) }\n    }\n}\n\nprivate extension Readout {\n    var viewId: Int { id.hashValue }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/InteractionBar/InteractionBarActionLabelView.swift",
    "content": "//\n//  InteractionBarActionView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/08/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct InteractionBarActionLabelView: View {\n    static let unweightedSymbols: Set<String> = [Icons.upvote, Icons.downvote]\n\n    @Setting(\\.a11y_showInteractionBarButtonBackground) var showInteractionBarButtonBackground\n        \n    let appearance: ActionAppearance\n    \n    init(_ appearance: ActionAppearance) {\n        self.appearance = appearance\n    }\n    \n    var body: some View {\n        Image(systemName: appearance.barIcon)\n            .resizable()\n            .fontWeight(Self.unweightedSymbols.contains(appearance.barIcon) ? .regular : .medium)\n            .symbolVariant(appearance.isOn ? .fill : .none)\n            .opacity(appearance.isInProgress ? 0 : 1)\n            .scaledToFit()\n            .frame(width: Constants.main.barIconSize, height: Constants.main.barIconSize)\n            .frame(width: Constants.main.barIconBackgroundSize, height: Constants.main.barIconBackgroundSize)\n            .foregroundStyle(appearance.isOn ? .themedContrastingLabel : .themedPrimary)\n            .background(appearance.isOn ? appearance.color : .clear, in: .rect(cornerRadius: Constants.main.barIconCornerRadius))\n            .background {\n                if showOutline {\n                    RoundedRectangle(cornerRadius: Constants.main.barIconCornerRadius)\n                        .fill(.themedTertiaryGroupedBackground)\n                        .paletteBorder(cornerRadius: Constants.main.barIconCornerRadius)\n                }\n            }\n            .frame(width: Constants.main.barIconHitbox, height: Constants.main.barIconHitbox)\n            .contentShape(Rectangle())\n            .opacity(appearance.isInProgress ? 0.5 : 1)\n            .overlay {\n                if appearance.isInProgress {\n                    ProgressView()\n                        .tint(appearance.isOn ? .themedContrastingLabel : .themedPrimary)\n                }\n            }\n            .transaction { $0.animation = nil }\n    }\n\n    var showOutline: Bool {\n        !appearance.isOn && showInteractionBarButtonBackground\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/InteractionBar/InteractionBarView.swift",
    "content": "//\n//  InteractionBarView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 14/06/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\n/// Renders an interaction bar.\n///\n/// This view makes several layout assumptions:\n/// - There will be no padding applied to this view\n/// - This view will always appear at the bottom of its visual container\n/// - The visual container of this view will have a padding of standardSpacing\nstruct InteractionBarView: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    private let leading: [EnrichedWidget]\n    private let trailing: [EnrichedWidget]\n    private let readouts: [Readout]\n    \n    init(\n        appState: AppState,\n        post: Post,\n        configuration: PostBarConfiguration,\n        navigation: NavigationLayer,\n        commentTreeTracker: CommentTreeTracker? = nil,\n        communityContext: Community? = nil,\n        reportContext: Report? = nil\n    ) {\n        self.leading = .init(\n            appState: appState,\n            navigation: navigation,\n            post: post,\n            items: configuration.leading,\n            commentTreeTracker: commentTreeTracker,\n            communityContext: communityContext,\n            reportContext: reportContext\n        )\n        self.trailing = .init(\n            appState: appState,\n            navigation: navigation,\n            post: post,\n            items: configuration.trailing,\n            commentTreeTracker: commentTreeTracker,\n            communityContext: communityContext,\n            reportContext: reportContext\n        )\n        \n        let associatedReadouts = configuration.all.reduce(into: Set<PostBarConfiguration.ReadoutType>()) { result, widget in\n            result.formUnion(widget.associatedReadouts(context: post))\n        }\n        self.readouts = configuration.readouts.compactMap { readout in\n            post.readout(type: readout, showColor: !associatedReadouts.contains(readout))\n        }\n    }\n    \n    init(\n        appState: AppState,\n        navigation: NavigationLayer,\n        comment: Comment,\n        configuration: CommentBarConfiguration,\n        commentTreeTracker: CommentTreeTracker? = nil,\n        communityContext: Community? = nil,\n        reportContext: Report?\n    ) {\n        self.leading = .init(\n            appState: appState,\n            navigation: navigation,\n            comment: comment,\n            items: configuration.leading,\n            commentTreeTracker: commentTreeTracker,\n            communityContext: communityContext,\n            reportContext: reportContext\n        )\n        self.trailing = .init(\n            appState: appState,\n            navigation: navigation,\n            comment: comment,\n            items: configuration.trailing,\n            commentTreeTracker: commentTreeTracker,\n            communityContext: communityContext,\n            reportContext: reportContext\n        )\n        let associatedReadouts = configuration.all.reduce(into: Set<CommentBarConfiguration.ReadoutType>()) { result, widget in\n            result.formUnion(widget.associatedReadouts(context: comment))\n        }\n        self.readouts = configuration.readouts.compactMap { readout in\n            comment.readout(type: readout, showColor: !associatedReadouts.contains(readout))\n        }\n    }\n    \n    init(\n        appState: AppState,\n        navigation: NavigationLayer,\n        comment: Comment,\n        notification: InboxNotification,\n        configuration: ReplyBarConfiguration\n    ) {\n        self.leading = .init(\n            appState: appState,\n            navigation: navigation,\n            comment: comment,\n            notification: notification,\n            items: configuration.leading\n        )\n        self.trailing = .init(\n            appState: appState,\n            navigation: navigation,\n            comment: comment,\n            notification: notification,\n            items: configuration.trailing\n        )\n        let associatedReadouts = configuration.all.reduce(into: Set<ReplyBarConfiguration.ReadoutType>()) { result, widget in\n            result.formUnion(widget.associatedReadouts(context: comment))\n        }\n        self.readouts = configuration.readouts.compactMap { readout in\n            comment.readout(type: readout, showColor: !associatedReadouts.contains(readout))\n        }\n    }\n\n    var body: some View {\n        HStack(spacing: 0) {\n            ForEach(leading, id: \\.viewId, content: widgetView)\n                .fixedSize(horizontal: true, vertical: false)\n            InfoStackView(readouts: readouts)\n                .frame(maxWidth: .infinity, alignment: infoStackAlignment)\n                .padding(.horizontal, Constants.main.standardSpacing)\n            ForEach(trailing, id: \\.viewId, content: widgetView)\n                .fixedSize(horizontal: true, vertical: false)\n        }\n        .frame(height: Constants.main.barIconHitbox)\n        .geometryGroup()\n    }\n    \n    var infoStackAlignment: Alignment {\n        switch (leading.isEmpty, trailing.isEmpty) {\n        case (true, false): .leading\n        case (false, true): .trailing\n        default: .center\n        }\n    }\n    \n    @ViewBuilder\n    private func widgetView(_ widget: EnrichedWidget) -> some View {\n        switch widget {\n        case let .action(action):\n            actionView(action)\n        case let .counter(counter):\n            counterView(counter)\n        }\n    }\n    \n    @ViewBuilder\n    private func counterView(_ counter: Counter) -> some View {\n        let paddingEdges: Edge.Set = {\n            if counter.leadingAction == nil { return .leading }\n            if counter.trailingAction == nil { return .trailing }\n            return []\n        }()\n        \n        HStack(spacing: 0) {\n            if let leadingAction = counter.leadingAction {\n                actionView(leadingAction)\n            }\n            Text(counter.value?.description ?? \"\")\n                .monospacedDigit()\n                .contentTransition(.numericText(value: Double(counter.value ?? 0)))\n                .animation(.default, value: counter.value)\n                .foregroundStyle(.themedPrimary)\n                .padding(paddingEdges, Constants.main.standardSpacing)\n                \n            if let trailingAction = counter.trailingAction {\n                actionView(trailingAction)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    private func actionView(_ action: any Action) -> some View {\n        Group {\n            if let action = action as? ActionGroup {\n                Menu {\n                    ForEach(action.children, id: \\.id) { child in\n                        MenuButton(action: child)\n                    }\n                } label: {\n                    InteractionBarActionLabelView(action.appearance)\n                        .opacity(action.disabled ? 0.5 : 1)\n                }\n                .onTapGesture {}\n            } else if let action = action as? BasicAction {\n                InteractionBarBasicButton(action: action)\n                    .popupAnchor()\n            }\n        }\n        .accessibilityLabel(action.appearance.label)\n        .accessibilityAction(.default) {\n            (action as? BasicAction)?.callback?()\n        }\n        .buttonStyle(.empty)\n        .disabled({\n            if let action = action as? BasicAction {\n                return action.callback == nil\n            } else {\n                return false\n            }\n        }())\n        .popupAnchor()\n    }\n}\n\nprivate struct InteractionBarBasicButton: View {\n    @Environment(PopupAnchorModel.self) var popupModel\n    \n    let action: BasicAction\n    \n    var body: some View {\n        Button {\n            action.callbackWithConfirmation(popupModel: popupModel)\n        } label: {\n            InteractionBarActionLabelView(action.appearance)\n                .opacity(action.disabled ? 0.5 : 1)\n        }\n    }\n}\n\nprivate enum EnrichedWidget {\n    case action(any Action)\n    case counter(Counter)\n    \n    var viewId: Int {\n        var hasher = Hasher()\n        switch self {\n        case let .action(action):\n            hasher.combine(1)\n            hasher.combine(action.id)\n            hasher.combine(action.appearance.isOn)\n            hasher.combine(action.appearance.isInProgress)\n            hasher.combine((action as? BasicAction)?.disabled)\n        case let .counter(counter):\n            // If `counter.value` is included in this, the fancy `.numericText()` transition\n            // won't work. In theory, you *do* need to include `counter.value` if you want a\n            // view update to happen when it changes... but one occurs anyway without doing that,\n            // so I'm hoping it'll be fine? The inclusion of `action.isOn` above is definitely\n            // needed. - Sjmarf 2024-06-15\n            hasher.combine(2)\n            hasher.combine(counter.leadingAction?.id)\n            hasher.combine(counter.trailingAction?.id)\n            hasher.combine((counter.leadingAction as? BasicAction)?.disabled)\n            hasher.combine((counter.trailingAction as? BasicAction)?.disabled)\n        }\n        return hasher.finalize()\n    }\n}\n\nextension [EnrichedWidget] {\n    init(\n        appState: AppState,\n        navigation: NavigationLayer,\n        post: Post,\n        items: [PostBarConfiguration.Item],\n        commentTreeTracker: CommentTreeTracker?,\n        communityContext: Community?,\n        reportContext: Report?\n    ) {\n        self = items.compactMap { item in\n            switch item {\n            case let .action(action):\n                if let action = post.action(\n                    appState: appState,\n                    navigation: navigation,\n                    type: action,\n                    commentTreeTracker: commentTreeTracker,\n                    communityContext: communityContext,\n                    reportContext: reportContext\n                ) {\n                    return .action(action)\n                }\n            case let .counter(counter):\n                if let counter = post.counter(appState: appState, type: counter, commentTreeTracker: commentTreeTracker) {\n                    return .counter(counter)\n                }\n            }\n            return nil\n        }\n    }\n    \n    init(\n        appState: AppState,\n        navigation: NavigationLayer,\n        comment: Comment,\n        items: [CommentBarConfiguration.Item],\n        commentTreeTracker: CommentTreeTracker?,\n        communityContext: Community?,\n        reportContext: Report?\n    ) {\n        self = items.compactMap { item in\n            switch item {\n            case let .action(action):\n                if let action = comment.action(\n                    appState: appState,\n                    type: action,\n                    navigation: navigation,\n                    commentTreeTracker: commentTreeTracker,\n                    communityContext: communityContext,\n                    reportContext: reportContext\n                ) {\n                    return .action(action)\n                }\n            case let .counter(counter):\n                if let counter = comment.counter(\n                    appState: appState,\n                    type: counter,\n                    commentTreeTracker: commentTreeTracker\n                ) {\n                    return .counter(counter)\n                }\n            }\n            return nil\n        }\n    }\n    \n    init(\n        appState: AppState,\n        navigation: NavigationLayer,\n        comment: Comment,\n        notification: InboxNotification,\n        items: [ReplyBarConfiguration.Item]\n    ) {\n        self = items.compactMap { item in\n            switch item {\n            case let .action(action):\n                if let action = comment.action(\n                    appState: appState,\n                    type: action,\n                    navigation: navigation,\n                    notification: notification\n                ) {\n                    return .action(action)\n                }\n            case let .counter(counter):\n                if let counter = comment.counter(appState: appState, type: counter) {\n                    return .counter(counter)\n                }\n            }\n            return nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/JumpButtonView.swift",
    "content": "//\n//  JumpButton.swift\n//  Mlem\n//\n//  Created by Sjmarf on 11/08/2023.\n//\n\nimport Haptics\nimport Icons\nimport SwiftUI\n\nstruct JumpButtonView: View {\n    @Environment(HapticManager.self) var hapticManager\n    \n    @State private var pressed: Bool = false\n    \n    var icon: Icon = .lemmy.jumpButton\n    var onShortPress: () -> Void\n    var onLongPress: (() -> Void)?\n    \n    var body: some View {\n        if #available(iOS 26, *) {\n            // using glassEffect rather than GlassButtonStyle because the button style is buggy\n            content\n                .tint(.primary)\n                .glassEffect(.regular.interactive(), in: .circle)\n                .padding(10)\n        } else {\n            content\n                .buttonStyle(.empty)\n        }\n    }\n    \n    var content: some View {\n        Button {} label: {\n            Image(icon: icon)\n                .fontWeight(.semibold)\n                .foregroundStyle(.secondary)\n                .aspectRatio(contentMode: .fit)\n                .frame(width: 44, height: 44)\n                .background {\n                    if !UIDevice.isIos26 {\n                        Circle()\n                            .stroke(.tertiary.opacity(0.3))\n                            .background(.bar)\n                            .clipShape(.circle)\n                    }\n                }\n                .padding(UIDevice.isIos26 ? 0 : 10)\n                .scaleEffect(pressed && !UIDevice.isIos26 ? 1.2 : 1.0)\n                .onTapGesture {\n                    hapticManager.play(haptic: .gentleInfo, tier: .high)\n                    onShortPress()\n                }\n                .onLongPressGesture(\n                    perform: {\n                        hapticManager.play(haptic: .gentleInfo, tier: .high)\n                        if let onLongPress {\n                            onLongPress()\n                        }\n                    },\n                    onPressingChanged: { pressing in\n                        withAnimation(.interactiveSpring()) {\n                            pressed = pressing\n                        }\n                    }\n                )\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Labels/FullyQualifiedLabelView.swift",
    "content": "//\n//  FullyQualifiedLabelView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-19.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nenum FullyQualifiedLabelStyle: CaseIterable {\n    case small\n    case medium\n    case large\n    \n    var avatarSize: CGFloat {\n        switch self {\n        case .small: Constants.main.smallAvatarSize\n        case .medium: Constants.main.mediumAvatarSize\n        case .large: Constants.main.largeAvatarSize\n        }\n    }\n    \n    var avatarResolution: Int {\n        switch self {\n        case .small: 32\n        case .medium: 64\n        case .large: 96\n        }\n    }\n    \n    var instanceLocation: InstanceLocation {\n        switch self {\n        case .small: .trailing\n        case .medium: .trailing\n        case .large: .bottom\n        }\n    }\n}\n\n/// View for rendering fully qualified labels (i.e., user or community names)\nstruct FullyQualifiedLabelView: View {\n    typealias Entity = CommunityOrPerson & ProfileProviding\n    \n    @Environment(AppState.self) var appState\n    @Environment(\\.postContext) var postContext: Post?\n    @Environment(\\.commentContext) var commentContext: Comment?\n    @Environment(\\.communityContext) var communityContext: Community?\n    @Environment(\\.feedContext) var feedContext: FeedContext?\n\n    @Setting(\\.post_showSubscribedStatus) var showSubscribedStatus\n    @Setting(\\.person_showAvatar) var showPersonAvatar\n    @Setting(\\.community_showAvatar) var showCommunityAvatar\n    \n    let entity: (any Entity)?\n    let avatarFallback: MediaView.Fallback\n    let labelStyle: FullyQualifiedLabelStyle\n    var showAvatar: Bool?\n    var showInstance: Bool = true\n    var showFlairs: Bool = true\n    var blurred: Bool = false\n    \n    var shouldShowAvatar: Bool {\n        if let showAvatar { return showAvatar }\n        \n        if entity is Community {\n            return showCommunityAvatar\n        } else {\n            return showPersonAvatar\n        }\n    }\n    \n    var showSubscriptionIndicator: Bool {\n        guard showSubscribedStatus,\n              entity is Community,\n              let userSession = appState.firstSession as? UserSession,\n              let communityId = postContext?.communityId,\n              let feedContextShowsIndicator = feedContext?.showSubscriptionIndicator else {\n            return false\n        }\n        \n        let subscribedToCommunity: Bool = userSession.subscriptions.communityIds.contains(communityId)\n        \n        return subscribedToCommunity && feedContextShowsIndicator\n    }\n    \n    @ScaledMetric(relativeTo: .body) var subscriptionIndicatorSize: CGFloat = 8.0\n    \n    var body: some View {\n        HStack(spacing: 7) {\n            if shouldShowAvatar {\n                CircleCroppedImageView(\n                    url: entity?.avatar?.withIconSize(labelStyle.avatarResolution),\n                    frame: labelStyle.avatarSize,\n                    fallback: avatarFallback,\n                    showProgress: false,\n                    blurred: blurred\n                )\n            }\n\n            VStack(alignment: .leading, spacing: 0) {\n                HStack(spacing: 4) {\n                    if showSubscriptionIndicator {\n                        Image(icon: .general.circle)\n                            .symbolVariant(.fill)\n                            .font(.system(size: subscriptionIndicatorSize))\n                            .foregroundStyle(.themedSecondary)\n                            .padding(.bottom, 2)\n                    }\n\n                    FullyQualifiedNameView(\n                        name: entity?.name,\n                        instance: entity?.host,\n                        instanceLocation: showInstance ? labelStyle.instanceLocation : .disabled,\n                        prependedText: flairs.textView\n                    )\n                    .symbolVariant(.fill)\n                }\n                .imageScale(.small)\n                .offset(y: 1)\n                if let note = (entity as? Person)?.note, feedContext != .person {\n                    self.note(text: note)\n                }\n            }\n        }\n        .accessibilityElement(children: .ignore)\n        .accessibilityLabel(accessibilityLabel)\n    }\n\n    func note(text: String) -> some View {\n        Text(text)\n            .font(.footnote)\n            .foregroundStyle(.secondary)\n}\n    \n    var flairs: [PersonFlair] {\n        guard showFlairs, let person = entity as? Person else { return [] }\n        return person.flairs(\n            interactableContext: interactableContext,\n            communityContext: communityContext\n        )\n    }\n    \n    var interactableContext: (any InteractableProviding)? {\n        guard let person = entity as? Person else { return nil }\n        if let commentContext,\n           let creator = commentContext.creator.value,\n           creator.actorId == person.actorId {\n            return commentContext\n        }\n        if let postContext,\n           let creator = postContext.creator.value,\n           creator.actorId == person.actorId {\n            return postContext\n        }\n        return nil\n    }\n    \n    var accessibilityLabel: String {\n        guard let entity else { return String(localized: \"Loading...\") }\n        let flairs = flairs\n\n        var result = entity.fullName\n        \n        if !flairs.isEmpty {\n            result += flairs.map { String(localized: $0.label) }.joined(separator: \", \")\n        }\n\n        if let note = (entity as? Person)?.note {\n            result += \"\\(String(localized: \"Note\")): \\(note)\"\n        }\n\n        return result\n    }\n}\n\nextension FullyQualifiedLabelView {\n    init(\n        _ entity: Person?,\n        labelStyle: FullyQualifiedLabelStyle,\n        showAvatar: Bool? = nil,\n        showInstance: Bool = true,\n        showFlairs: Bool = true,\n        blurred: Bool = false\n    ) {\n        self.init(\n            entity: entity,\n            avatarFallback: .personAvatar,\n            labelStyle: labelStyle,\n            showAvatar: showAvatar,\n            showInstance: showInstance,\n            showFlairs: showFlairs,\n            blurred: blurred\n        )\n    }\n    \n    init(\n        _ entity: Community?,\n        labelStyle: FullyQualifiedLabelStyle,\n        showAvatar: Bool? = nil,\n        showInstance: Bool = true,\n        showFlairs: Bool = true,\n        blurred: Bool = false\n    ) {\n        self.init(\n            entity: entity,\n            avatarFallback: .communityAvatar,\n            labelStyle: labelStyle,\n            showAvatar: showAvatar,\n            showInstance: showInstance,\n            showFlairs: showFlairs,\n            blurred: blurred\n        )\n    }\n    \n    init(\n        _ entity: UserAccount?,\n        labelStyle: FullyQualifiedLabelStyle,\n        showAvatar: Bool? = nil,\n        showInstance: Bool = true,\n        showFlairs: Bool = true,\n        blurred: Bool = false\n    ) {\n        self.init(\n            entity: entity,\n            avatarFallback: .personAvatar,\n            labelStyle: labelStyle,\n            showAvatar: showAvatar,\n            showInstance: showInstance,\n            showFlairs: showFlairs,\n            blurred: blurred\n        )\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(\"Sizes\", traits: .sampleEnvironment, .sizeThatFitsLayout) {\n//        VStack(alignment: .leading) {\n//            ForEach(FullyQualifiedLabelStyle.allCases, id: \\.self) { style in\n//                FullyQualifiedLabelView(Person1.mock(.generic), labelStyle: style)\n//            }\n//        }\n//        .padding()\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Labels/FullyQualifiedLinkView.swift",
    "content": "//\n//  FullyQualifiedLinkView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 01/06/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct FullyQualifiedLinkView: View {\n    @Environment(NavigationLayer.self) private var navigation\n    \n    let entity: (any FullyQualifiedLabelView.Entity)?\n    let avatarFallback: MediaView.Fallback\n    let labelStyle: FullyQualifiedLabelStyle\n    var showAvatar: Bool?\n    var showInstance: Bool = true\n    var blurred: Bool = false\n    \n    @State private var id = UUID()\n    \n    var body: some View {\n        Button {\n            if let person = entity as? Person {\n                navigation.push(.person(person))\n            } else if let community = entity as? Community {\n                navigation.push(.community(community))\n            }\n        } label: {\n            FullyQualifiedLabelView(\n                entity: entity,\n                avatarFallback: avatarFallback,\n                labelStyle: labelStyle,\n                showAvatar: showAvatar,\n                showInstance: showInstance,\n                blurred: blurred\n            )\n        }\n        .buttonStyle(.plain)\n        .id(id)\n    }\n}\n\nextension FullyQualifiedLinkView {\n    init(\n        _ entity: Person?,\n        labelStyle: FullyQualifiedLabelStyle,\n        showAvatar: Bool? = nil,\n        showInstance: Bool = true,\n        blurred: Bool = false\n    ) {\n        self.init(\n            entity: entity,\n            avatarFallback: .personAvatar,\n            labelStyle: labelStyle,\n            showAvatar: showAvatar,\n            showInstance: showInstance,\n            blurred: blurred\n        )\n    }\n    \n    init(\n        _ entity: Community?,\n        labelStyle: FullyQualifiedLabelStyle,\n        showAvatar: Bool? = nil,\n        showInstance: Bool = true,\n        blurred: Bool = false\n    ) {\n        self.init(\n            entity: entity,\n            avatarFallback: .communityAvatar,\n            labelStyle: labelStyle,\n            showAvatar: showAvatar,\n            showInstance: showInstance,\n            blurred: blurred\n        )\n    }\n    \n    init(\n        _ entity: UserAccount?,\n        labelStyle: FullyQualifiedLabelStyle,\n        showAvatar: Bool? = nil,\n        showInstance: Bool = true,\n        blurred: Bool = false\n    ) {\n        self.init(\n            entity: entity,\n            avatarFallback: .personAvatar,\n            labelStyle: labelStyle,\n            showAvatar: showAvatar,\n            showInstance: showInstance,\n            blurred: blurred\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Labels/FullyQualifiedNameView.swift",
    "content": "//\n//  FullyQualifiedNameView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-05-19.\n//\n\nimport ComponentViews\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nenum InstanceLocation: String, CaseIterable, Codable {\n    case disabled\n    case trailing\n    case bottom\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .disabled: \"Disabled\"\n        case .trailing: \"Trailing\"\n        case .bottom: \"Bottom\"\n        }\n    }\n}\n\nstruct FullyQualifiedNameView: View {\n    // parameters\n    let name: String?\n    let instance: String?\n    let instanceLocation: InstanceLocation\n    var prependedText: Text = .init(verbatim: \"\")\n    \n    // scale placeholder capsule height and spacing according to font size\n    @ScaledMetric(relativeTo: .footnote) var capsuleHeight: CGFloat = 13\n    @ScaledMetric(relativeTo: .footnote) var capsuleSpacing: CGFloat = 5\n    \n    var body: some View {\n        if let name, let instance {\n            (prependedText + nameText(name: name) + instanceText(instance: instance))\n                .lineLimit(instanceLocation == .bottom ? 2 : 1)\n                .font(.footnote)\n                .multilineTextAlignment(.leading)\n                .environment(\\._lineHeightMultiple, 0.8)\n        } else {\n            placeholder\n        }\n    }\n    \n    func nameText(name: String) -> Text {\n        Text(name)\n            .bold()\n            .foregroundStyle(.themedSecondary)\n    }\n    \n    func instanceText(instance: String) -> Text {\n        if instanceLocation != .disabled {\n            // prepend a newline if location is bottom for easy concatenation\n            Text(verbatim: \"\\(instanceLocation == .bottom ? \"\\n\" : \"\")@\\(instance)\")\n                .font(.footnote)\n                .foregroundStyle(.themedTertiary)\n        } else {\n            Text(verbatim: \"\") // return empty Text for easy concatenation\n        }\n    }\n    \n    var placeholder: some View {\n        VStack(alignment: .leading, spacing: capsuleSpacing) {\n            MockTextView()\n                .frame(width: instanceLocation == .bottom ? 100 : 160, height: capsuleHeight)\n            \n            if instanceLocation == .bottom {\n                MockTextView()\n                    .frame(width: 60, height: capsuleHeight * 0.8)\n                    .padding(.vertical, capsuleHeight * 0.2)\n            }\n        }\n    }\n}\n\nextension FullyQualifiedNameView {\n    init(_ entity: any CommunityOrPerson, instanceLocation: InstanceLocation) {\n        self.init(name: entity.name, instance: entity.host, instanceLocation: instanceLocation)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/LinkHostView.swift",
    "content": "//\n//  LinkHostView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-07.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct LinkHostView: View {\n    @Setting(\\.post_webPreview_showIcon) var showFavicons\n    \n    let link: PostLink\n    let withCapsule: Bool\n    \n    var body: some View {\n        if withCapsule {\n            content\n                .padding(Constants.main.halfSpacing)\n                .padding(showFavicons ? .trailing : .horizontal, 3)\n                .background {\n                    Capsule()\n                        .fill(.regularMaterial)\n                        .overlay(Capsule().fill(.themedBackground.opacity(0.25)))\n                }\n        } else {\n            content\n        }\n    }\n    \n    var content: some View {\n        HStack(spacing: Constants.main.halfSpacing) {\n            if showFavicons {\n                CircleCroppedImageView(url: link.favicon, frame: Constants.main.smallAvatarSize, fallback: .favicon)\n            }\n            \n            Text(link.host)\n                .foregroundStyle(.themedSecondary)\n        }\n        .font(.footnote)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ListRow/AccountListRow.swift",
    "content": "//\n//  AccountListRow.swift\n//  Mlem\n//\n//  Created by Sjmarf on 22/12/2023.\n//\n\nimport Dependencies\nimport Icons\nimport MlemMiddleware\nimport NukeUI\nimport SwiftUI\n\nstruct AccountListRow: View {\n    @Environment(\\.dismiss) private var dismiss\n    \n    @Environment(AppState.self) private var appState\n    @Environment(NavigationLayer.self) private var navigation\n    @Setting(\\.accounts_keepPlace) var keepPlace\n    \n    @State private var showingSignOutConfirmation: Bool = false\n    \n    let account: any Account\n    var unreadCount: Int?\n    var responseTime: TimeInterval?\n    var complications: Set<AccountListRowBody.Complication> = .instanceAndTime\n    @Binding var isSwitching: Bool\n    \n    var body: some View {\n        Button {\n            if appState.firstSession.actorId != account.actorId {\n                changeAccount(keepPlace: keepPlace)\n            }\n        } label: {\n            AccountListRowBody(\n                account: account,\n                unreadCount: unreadCount,\n                responseTime: responseTime,\n                complications: complications\n            )\n        }\n        .buttonStyle(.plain)\n        .accessibilityLabel(accessibilityLabel)\n        .swipeActions(edge: .leading, allowsFullSwipe: true) {\n            if appState.firstSession.actorId != account.actorId {\n                Group {\n                    if keepPlace {\n                        Button(\"Reload\", icon: .lemmy.switchAccountAndReload) {\n                            changeAccount(keepPlace: false)\n                        }\n                        .buttonStyle(.automatic)\n                    } else {\n                        Button(\"Keep Place\", icon: .lemmy.switchAccountAndKeepPlace) {\n                            changeAccount(keepPlace: true)\n                        }\n                        .buttonStyle(.automatic)\n                    }\n                }\n                .tint(.blue)\n            }\n        }\n        .swipeActions(edge: .trailing, allowsFullSwipe: false) {\n            if (account as? GuestAccount)?.isSaved ?? true {\n                Button(String(localized: signOutLabel)) {\n                    showingSignOutConfirmation = true\n                }\n                .buttonStyle(.automatic)\n                .tint(.red)\n            }\n        }\n        .contextMenu {\n            if (account as? GuestAccount)?.isSaved ?? true {\n                SwiftUI.Section(\"Switch to this account and...\") {\n                    Button(\"Reload\", icon: .lemmy.switchAccountAndReload) {\n                        changeAccount(keepPlace: false)\n                    }\n                    Button(\"Keep Place\", icon: .lemmy.switchAccountAndKeepPlace) {\n                        changeAccount(keepPlace: true)\n                    }\n                }\n                .disabled(appState.firstSession.actorId == account.actorId)\n                Divider()\n                Button(signOutLabel, icon: .general.signOut, role: .destructive) {\n                    showingSignOutConfirmation = true\n                }\n            } else {\n                Button(\"Keep\", icon: .lemmy.addPin) {\n                    AccountsTracker.main.addAccount(account: account)\n                }\n            }\n        }\n        .labelStyle(.titleAndIcon) // Override `.conditional` label style from parent view\n        .confirmationDialog(String(localized: signOutPrompt), isPresented: $showingSignOutConfirmation) {\n            Button(String(localized: signOutLabel), role: .destructive) {\n                if navigation.isInsideSheet, appState.activeSessions.contains(where: { $0.account === account }) {\n                    dismiss()\n                }\n                account.signOut()\n            }\n        } message: {\n            Text(signOutPrompt)\n        }\n    }\n    \n    func changeAccount(keepPlace: Bool) {\n        appState.changeAccount(to: account, keepPlace: keepPlace)\n        if navigation.isInsideSheet {\n            if keepPlace {\n                dismiss()\n            } else {\n                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                    dismiss()\n                }\n            }\n        }\n    }\n    \n    var signOutLabel: LocalizedStringResource {\n        account is UserAccount ? \"Sign Out\" : \"Remove\"\n    }\n    \n    var signOutPrompt: LocalizedStringResource {\n        if account is UserAccount {\n            \"Really sign out of \\(account.nickname)?\"\n        } else {\n            \"Really remove \\(account.nickname)?\"\n        }\n    }\n    \n    var accessibilityLabel: String {\n        var text: String\n        if let account = account as? UserAccount {\n            text = account.fullName ?? \"unknown\"\n        } else {\n            text = \"guest\"\n        }\n        \n        if appState.firstSession.actorId == account.actorId {\n            text += \", active\"\n        }\n        return text\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ListRow/AccountListRowBody.swift",
    "content": "//\n//  AccountListRowBody.swift\n//  Mlem\n//\n//  Created by Sjmarf on 24/05/2024.\n//\n\nimport NukeUI\nimport SwiftUI\n\nstruct AccountListRowBody: View {\n    @Environment(AppState.self) private var appState\n    \n    enum Complication: CaseIterable {\n        case instance, lastUsed, responseTime, isActive, unreadCount\n    }\n    \n    let account: any Account\n    var unreadCount: Int?\n    var responseTime: TimeInterval?\n    var complications: Set<Complication> = .instanceAndTime\n    \n    var body: some View {\n        HStack(alignment: .center, spacing: 10) {\n            CircleCroppedImageView(account, frame: 40, showProgress: false)\n                .padding(.leading, -5)\n            VStack(alignment: .leading) {\n                Text(account.nickname)\n                if let captionText {\n                    Text(captionText)\n                        .font(.footnote)\n                        .foregroundStyle(.secondary)\n                }\n            }\n            .padding(.vertical, -2)\n            Spacer()\n            AccountListRowBodyReadoutView(\n                isActive: appState.firstSession.actorId == account.actorId,\n                unreadCount: unreadCount,\n                complications: complications\n            )\n        }\n        .contentShape(.rect)\n        .animation(.easeOut(duration: 0.1), value: animationHash)\n    }\n    \n    var animationHash: Int {\n        var hasher = Hasher()\n        hasher.combine(unreadCount)\n        hasher.combine(responseTime)\n        return hasher.finalize()\n    }\n    \n    var timeText: String? {\n        switch account.activityState {\n        case let .inactive(lastUsed):\n            guard let lastUsed else { return nil }\n            if abs(lastUsed.timeIntervalSinceNow) < 5 {\n                return .init(localized: \"Just Now\")\n            }\n            let formatter = RelativeDateTimeFormatter()\n            formatter.unitsStyle = .short\n            return formatter.localizedString(for: lastUsed, relativeTo: Date.now)\n        case .active:\n            return .init(localized: \"Now\")\n        }\n    }\n    \n    var captionText: String? {\n        var output: [String] = []\n        if complications.contains(.instance) {\n            if account is GuestAccount {\n                output.append(.init(localized: \"Guest\"))\n            } else {\n                output.append(\"@\\(account.api.host)\")\n            }\n        }\n        if complications.contains(.lastUsed), let timeText {\n            if (account as? GuestAccount)?.isSaved ?? true {\n                output.append(timeText)\n            } else {\n                output.append(.init(localized: \"Temporary\"))\n            }\n        }\n        if complications.contains(.responseTime), let responseTime {\n            let measurement = Measurement(value: Double(Int(responseTime * 1000)), unit: UnitDuration.milliseconds)\n            let formatter = MeasurementFormatter()\n            formatter.unitOptions = .providedUnit\n            formatter.unitStyle = .short\n            output.append(formatter.string(from: measurement))\n        }\n        return output.joined(separator: \" • \")\n    }\n}\n\nprivate struct AccountListRowBodyReadoutView: View {\n    let isActive: Bool\n    let unreadCount: Int?\n    let complications: Set<AccountListRowBody.Complication>\n    \n    var body: some View {\n        if complications.contains(.isActive), isActive {\n            Image(icon: .general.circle)\n                .symbolVariant(.fill)\n                .foregroundStyle(.themedPositive)\n                .font(.system(size: 10.0))\n                .padding(.trailing, 7)\n        } else {\n            Image(icon: .lemmy.notificationCount(unreadCount ?? 0))\n                .foregroundStyle(.themedContrastingLabel, .themedWarning)\n                .imageScale(.large)\n                // For some reason, the animations don't work if we use an `if` statement\n                .opacity(unreadCount == nil ? 0 : 1)\n        }\n    }\n}\n\nextension Set<AccountListRowBody.Complication> {\n    static let instanceAndTime: Self = [.instance, .lastUsed, .isActive, .unreadCount]\n    static let instanceOnly: Self = [.instance, .isActive, .unreadCount]\n    static let timeOnly: Self = [.lastUsed, .isActive, .unreadCount]\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/MarkdownEditorToolbarView.swift",
    "content": "//\n//  MarkdownEditorToolbarView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/07/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\n// These can't be passed directly to the constructor - SwiftUI doesn't pick up on\n// view updates, because of the way this view is nested inside the keyboard using UIKit.\n@Observable\nclass MarkdownEditorToolbarModel {\n    var imageUploadApi: ApiClient?\n}\n\nstruct MarkdownEditorToolbarView: View {\n    enum AvailableActions {\n        case all, inlineOnly\n    }\n    \n    @Environment(NavigationLayer.self) var navigation\n    \n    let actions: AvailableActions\n    let textView: UITextView\n    \n    let model: MarkdownEditorToolbarModel\n    let uploadHistory: ImageUploadHistoryManager\n    \n    @State var imageManager: ImageUploadManager = .init()\n    @ScaledMetric(relativeTo: .body) var toolbarHeight: CGFloat = 32\n    \n    @State var leftFade: Bool\n    @State var rightFade: Bool\n    \n    init(\n        showing actions: AvailableActions = .all,\n        textView: UITextView,\n        uploadHistory: ImageUploadHistoryManager = .init(),\n        model: MarkdownEditorToolbarModel\n    ) {\n        self.actions = actions\n        self.textView = textView\n        self.uploadHistory = uploadHistory\n        self.model = model\n        \n        self.leftFade = false\n        if #available(iOS 18.0, *) {\n            self.rightFade = true\n        } else {\n            self.rightFade = false\n        }\n    }\n    \n    @ViewBuilder\n    var body: some View {\n        Group {\n            switch imageManager.state {\n            case let .uploading(progress):\n                if progress == 1 {\n                    HStack {\n                        Text(\"Uploading...\")\n                        ProgressView()\n                            .tint(.themedSecondary)\n                    }\n                } else {\n                    ProgressView(value: progress)\n                        .progressViewStyle(.linear)\n                        .padding(.horizontal)\n                }\n            default:\n                if #available(iOS 26, *) {\n                    content\n                        .compositingGroup()\n                        .glassEffect(.regular.interactive(), in: .capsule)\n                        .padding(.horizontal, 10)\n                        .padding(.bottom, 7)\n                } else {\n                    content\n                }\n            }\n        }\n        .frame(maxWidth: .infinity)\n        .frame(height: toolbarHeight, alignment: .bottom)\n        .padding(.top, UIDevice.isIos26 ? 12 : 0)\n        .onChange(of: imageManager.state) {\n            switch imageManager.state {\n            case let .done(upload):\n                if let range = textView.selectedTextRange {\n                    textView.replace(range, withText: \"![](\\(upload.url.absoluteString))\")\n                    uploadHistory.add(upload)\n                    imageManager.clear()\n                }\n            default:\n                break\n            }\n        }\n    }\n    \n    var content: some View {\n        ScrollView(.horizontal) {\n            if !UIDevice.isIos26 {\n                Spacer()\n            }\n            HStack(spacing: 16) {\n                scrollContent\n            }\n            .imageScale(.large)\n            .buttonStyle(.plain)\n            .foregroundStyle(.secondary)\n            .labelStyle(.iconOnly)\n            .padding(.horizontal)\n            .padding(UIDevice.isIos26 ? .vertical : .bottom, UIDevice.isIos26 ? 5 : 2)\n        }\n        .scrollIndicators(.hidden)\n        .mask(\n            HStack(spacing: 0) {\n                LinearGradient(\n                    gradient: Gradient(colors: [Color.black.opacity(leftFade ? 0 : 1), Color.black]),\n                    startPoint: .leading,\n                    endPoint: .trailing\n                )\n                .frame(width: 100)\n                \n                Rectangle().fill(Color.black)\n                \n                LinearGradient(\n                    gradient: Gradient(colors: [Color.black, Color.black.opacity(rightFade ? 0 : 1)]),\n                    startPoint: .leading,\n                    endPoint: .trailing\n                )\n                .frame(width: 100)\n            }\n        )\n        .task(id: model.imageUploadApi) {\n            do {\n                try await model.imageUploadApi?.ensureContextPresence()\n            } catch {\n                handleError(error)\n            }\n        }\n    }\n\n    @ViewBuilder\n    var scrollContent: some View {\n        // iPad already shows these buttons\n        if !UIDevice.isPad {\n            Button(\"Undo\", systemImage: \"arrow.uturn.backward\") {\n                textView.undoManager?.undo()\n            }\n            .compatibilityOnScrollVisibilityChange { isVisible in\n                withAnimation {\n                    leftFade = !isVisible\n                }\n            }\n            Button(\"Redo\", systemImage: \"arrow.uturn.forward\") {\n                textView.undoManager?.redo()\n            }\n            SwiftUI.Divider()\n                .padding(.top, 2)\n        }\n        Button(\"Bold\", icon: .markdown.bold) {\n            textView.wrapSelectionWithDelimiters(\"**\")\n        }\n        .compatibilityOnScrollVisibilityChange { isVisible in\n            if UIDevice.isPad {\n                withAnimation {\n                    leftFade = !isVisible\n                }\n            }\n        }\n        Button(\"Italic\", icon: .markdown.italic) {\n            textView.wrapSelectionWithDelimiters(\"_\")\n        }\n        Button(\"Strikethrough\", icon: .markdown.strikethrough) {\n            textView.wrapSelectionWithDelimiters(\"~~\")\n        }\n        Button(\"Superscript\", icon: .markdown.superscript) {\n            textView.wrapSelectionWithDelimiters(\"^\")\n        }\n        Button(\"Subscript\", icon: .markdown.subscript) {\n            textView.wrapSelectionWithDelimiters(\"~\")\n        }\n        Button(\"Code\", icon: .markdown.inlineCode) {\n            textView.wrapSelectionWithDelimiters(\"`\")\n        }\n        Button(\"Link\", icon: .markdown.insertLink) {\n            textView.wrapSelectionWithLink()\n        }\n        if actions == .all {\n            SwiftUI.Divider()\n                .padding(.top, 2)\n            Menu(\"Heading\", icon: .markdown.heading) {\n                ForEach(1 ..< 7) { level in\n                    Button(\"Heading \\(level)\") {\n                        textView.toggleHeadingAtCursor(level: level)\n                    }\n                }\n            }\n            Button(\"Quote\", icon: .markdown.quote) {\n                textView.toggleQuoteAtCursor()\n            }\n            if let imageUploadApi = model.imageUploadApi {\n                ImageUploadMenu(imageManager: imageManager, imageUploadApi: imageUploadApi) {\n                    Label(\"Image\", icon: .markdown.uploadImage)\n                }\n                .disabled(!imageUploadApi.contextIsFetched)\n            }\n            Button(\"Spoiler\", icon: .markdown.spoiler) {\n                textView.wrapSelectionWithSpoiler()\n            }\n            Button(\"Code Block\", icon: .markdown.codeBlock) {\n                textView.wrapSelectionWithCodeBlock()\n            }\n        }\n        SwiftUI.Divider()\n            .padding(.top, 2)\n        Button(\"Community Link\", icon: .lemmy.community) {\n            navigation.openSheet(.communityPicker { community in\n                textView.insertText(community.fullNameWithPrefix)\n            })\n        }\n        Button(\"User Link\", icon: .lemmy.person) {\n            navigation.openSheet(.personPicker { person in\n                // lemmy-ui doesn't recognize the @user@example.com format, so we have to do this instead :(\n                // See this issue https://github.com/LemmyNet/lemmy-ui/issues/2579\n                textView.insertText(\"[\\(person.fullNameWithPrefix)](\\(person.actorId))\")\n            })\n        }\n        Button(\"Instance Link\", icon: .lemmy.instance) {\n            navigation.openSheet(.instancePicker { instance in\n                textView.insertText(\"[\\(instance.host)](https://\\(instance.host))\")\n            })\n        }\n        .compatibilityOnScrollVisibilityChange { isVisible in\n            withAnimation {\n                rightFade = !isVisible\n            }\n        }\n    }\n}\n\nprivate extension View {\n    /// If onScrollVisibilityChange is available, applies it to this view; otherwise has no effect.\n    func compatibilityOnScrollVisibilityChange(_ action: @escaping (Bool) -> Void) -> some View {\n        if #available(iOS 18.0, *) {\n            return onScrollVisibilityChange(action)\n        } else {\n            return self\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/MarkdownTextEditor.swift",
    "content": "//\n//  MarkdownTextEditor.swift\n//  Mlem\n//\n//  Created by Sjmarf on 14/07/2024.\n//\n\nimport SwiftUI\n\nstruct MarkdownTextEditor<Content: View>: UIViewRepresentable {\n    let content: Content\n    let insets: UIEdgeInsets\n    let firstResponder: Bool\n    let prompt: String\n    let textView: UITextView\n    let placeholderLabel: UILabel = .init()\n    let font: UIFont\n    let sizingOffset: CGFloat\n    \n    let onChange: (String) -> Void\n    let onBeginEditing: () -> Void\n    \n    init(\n        // A binding isn't used here because it creates a view update every time\n        // the text changes. This created a noticable lag between pressing a key\n        // and it appearing on the screen. Instead, parent views can access the\n        // text directly from the `textView` and/or perform logic using the below\n        // `onChange` callback.\n        onChange: @escaping (String) -> Void = { _ in },\n        onBeginEditing: @escaping () -> Void = {},\n        prompt: LocalizedStringResource,\n        textView: UITextView,\n        font: UIFont = .preferredFont(forTextStyle: .body),\n        insets: UIEdgeInsets = .init(\n            top: Constants.main.halfSpacing,\n            left: Constants.main.standardSpacing,\n            bottom: Constants.main.standardSpacing,\n            right: Constants.main.standardSpacing\n        ),\n        firstResponder: Bool = true,\n        // In forms this needs to be set to ~10 (I don't know why this is the case)\n        sizingOffset: CGFloat = 1,\n        @ViewBuilder content: () -> Content\n    ) {\n        self.prompt = String(localized: prompt)\n        self.textView = textView\n        self.content = content()\n        self.font = font\n        self.insets = insets\n        self.firstResponder = firstResponder\n        self.sizingOffset = sizingOffset\n        self.onChange = onChange\n        self.onBeginEditing = onBeginEditing\n    }\n \n    func makeCoordinator() -> Coordinator {\n        Coordinator(self)\n    }\n \n    func makeUIView(context: Context) -> UITextView {\n        textView.font = font\n        textView.textContainerInset = insets\n        textView.delegate = context.coordinator\n        textView.translatesAutoresizingMaskIntoConstraints = false\n        textView.setContentHuggingPriority(.defaultLow, for: .horizontal)\n        textView.sizeToFit()\n        if firstResponder {\n            textView.becomeFirstResponder()\n        }\n        textView.backgroundColor = .clear\n        \n        let contentController = UIHostingController(\n            // If we don't explicitly set the environment here the toolbar can't access it\n            rootView: content.environment(context.environment[NavigationLayer.self])\n        )\n        let contentView = contentController.view!\n        \n        let inputView = UIInputView(frame: CGRect(x: 0, y: 0, width: 0, height: UIDevice.isIos26 ? 48 : 36), inputViewStyle: .keyboard)\n        inputView.addSubview(contentController.view)\n        inputView.inputViewController?.addChild(contentController)\n        contentView.translatesAutoresizingMaskIntoConstraints = false\n        contentView.backgroundColor = UIColor.clear\n        contentView.rightAnchor.constraint(equalTo: inputView.rightAnchor).isActive = true\n        contentView.leftAnchor.constraint(equalTo: inputView.leftAnchor).isActive = true\n        textView.inputAccessoryView = inputView\n        inputView.sizeToFit()\n        contentController.view.sizeToFit()\n    \n        placeholderLabel.text = prompt\n        placeholderLabel.font = textView.font\n        placeholderLabel.sizeToFit()\n        textView.addSubview(placeholderLabel)\n        placeholderLabel.frame.origin = CGPoint(\n            x: insets.left + 5,\n            y: insets.top + 1\n        )\n        placeholderLabel.textColor = UIColor(context.environment.palette.label.tertiary)\n        placeholderLabel.isHidden = !textView.text.isEmpty\n        \n        // Makes the text wrap instead of going off-screen\n        textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)\n\n        textView.isScrollEnabled = false\n             \n        return textView\n    }\n \n    func updateUIView(_ textView: UITextView, context: Context) {\n        textView.sizeToFit()\n    }\n    \n    func sizeThatFits(_ proposal: ProposedViewSize, uiView textView: UITextView, context: Context) -> CGSize? {\n        let dimensions = proposal.replacingUnspecifiedDimensions(\n            by: .init(\n                width: 0,\n                height: CGFloat.greatestFiniteMagnitude\n            )\n        )\n        textView.sizeToFit()\n\n        // `textView.contentSize` varies slightly on one line depending on which characters are typed.\n        // To avoid this we get the line height from the font and round `contentSize` to the nearest line.\n\n        let lineHeight = font.lineHeight + font.leading\n        \n        // This value seems to be constant no matter the font size\n        let constant: CGFloat = 15\n        let calculatedHeight = constant + round((textView.contentSize.height - constant) / lineHeight) * lineHeight\n          \n        // The \"+ 1\" fixes a bug in which there wouldn't be enough room to render a second line when using\n        // certain fonts (specifically, `.title2`). This would cause lines to sometimes not render. This\n        // is probably a result of floating point error or something like that. This bug isn't a result\n        // of the rounding logic above; it still happens when simply using `contentSize`.\n        return .init(\n            width: dimensions.width,\n            height: calculatedHeight + sizingOffset\n        )\n    }\n \n    class Coordinator: NSObject, UITextViewDelegate {\n        var parent: MarkdownTextEditor\n \n        init(_ textView: MarkdownTextEditor) {\n            self.parent = textView\n        }\n \n        func textViewDidChange(_ textView: UITextView) {\n            parent.onChange(textView.text)\n            parent.placeholderLabel.isHidden = !textView.text.isEmpty\n            textView.sizeToFit()\n        }\n        \n        func textViewDidBeginEditing(_ textView: UITextView) {\n            parent.placeholderLabel.isHidden = !textView.text.isEmpty\n            parent.onBeginEditing()\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/MarkdownWithLinkList.swift",
    "content": "//\n//  MarkdownWithLinkList.swift\n//  Mlem\n//\n//  Created by Sjmarf on 12/10/2024.\n//\n\nimport LemmyMarkdownUI\nimport SwiftUI\n\nstruct MarkdownWithLinkList: View {\n    @Environment(\\.palette) var palette\n    @Environment(\\.openURL) var openURL\n    @Environment(\\.scrollProxy) var scrollProxy\n    \n    @Setting(\\.links_displayMode) var tappableLinksDisplayMode\n    \n    @State var linksCollapsed: Bool = true\n    \n    let blocks: [BlockNode]\n    let markdownConfiguration: MarkdownConfigurationType\n    let showLinkCaptions: Bool\n    \n    init(\n        _ blocks: [BlockNode],\n        configuration: MarkdownConfigurationType = .default,\n        showLinkCaptions: Bool = true\n    ) {\n        self.blocks = blocks\n        self.markdownConfiguration = configuration\n        self.showLinkCaptions = showLinkCaptions\n    }\n    \n    init(\n        _ markdown: String,\n        configuration: MarkdownConfigurationType = .default,\n        showLinkCaptions: Bool = true\n    ) {\n        self.blocks = .init(markdown)\n        self.markdownConfiguration = configuration\n        self.showLinkCaptions = showLinkCaptions\n    }\n    \n    var showSubtitle: Bool {\n        tappableLinksDisplayMode == .large || tappableLinksDisplayMode == .contextual && showLinkCaptions\n    }\n    \n    var body: some View {\n        VStack(spacing: Constants.main.standardSpacing) {\n            Markdown(blocks, configuration: .init(type: markdownConfiguration, palette: palette))\n            if tappableLinksDisplayMode != .disabled {\n                linksView(blocks.links.filter { !$0.insideSpoiler })\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func linksView(_ linksData: [LinkData]) -> some View {\n        if linksData.count > 3 {\n            ForEach(Array(linksData[0 ..< 3].enumerated()), id: \\.offset) { _, link in\n                linkView(link)\n            }\n            \n            if linksCollapsed {\n                Button {\n                    withAnimation {\n                        linksCollapsed = false\n                    }\n                } label: {\n                    FooterLinkView(title: String(localized: \"\\(linksData.count - 3) more links...\"), subtitle: nil)\n                }\n            }\n            \n            if !linksCollapsed {\n                ForEach(Array(linksData[3...].enumerated()), id: \\.offset) { _, link in\n                    linkView(link)\n                }\n                \n                Button {\n                    withAnimation {\n                        linksCollapsed = true\n                        scrollProxy?.scrollTo(2)\n                    }\n                } label: {\n                    FooterLinkView(title: String(localized: \"Hide links\"), subtitle: nil)\n                }\n            }\n        } else {\n            ForEach(Array(linksData.enumerated()), id: \\.offset) { _, link in\n                linkView(link)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func linkView(_ data: LinkData) -> some View {\n        FooterLinkView(\n            title: data.stringTitle,\n            subtitle: showSubtitle ? data.url.absoluteURL.description : nil\n        )\n        .contextMenu {\n            Button(\"Open\", icon: .general.browser) {\n                openURL(data.url)\n            }\n            Button(\"Copy\", icon: .general.copy) {\n                let pasteboard = UIPasteboard.general\n                pasteboard.url = data.url\n            }\n            ShareLink(item: data.url)\n        } preview: {\n            WebView(url: data.url)\n        }\n        .onTapGesture {\n            openURL(data.url)\n        }\n    }\n}\n\nprivate extension LinkData {\n    var stringTitle: String {\n        let literal = title.stringLiteral\n        if literal == url.absoluteString {\n            return url.host() ?? literal\n        }\n        return literal\n    }\n}\n\nenum TappableLinksDisplayMode: String, Codable, CaseIterable {\n    case disabled, large, compact, contextual\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/MenuButton.swift",
    "content": "//\n//  MenuButton.swift\n//  Mlem\n//\n//  Created by Sjmarf on 31/03/2024.\n//\n\nimport SwiftUI\n\nstruct MenuButton: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(PopupAnchorModel.self) var popupModel: PopupAnchorModel?\n    \n    let action: any Action\n    \n    init(action: any Action) {\n        self.action = action\n    }\n    \n    var body: some View {\n        if let action = action as? BasicAction {\n            Button(\n                action.appearance.label,\n                systemImage: action.appearance.menuIcon,\n                role: action.appearance.isDestructive ? .destructive : nil,\n                action: {\n                    if let popupModel {\n                        action.callbackWithConfirmation(popupModel: popupModel)\n                    } else {\n                        assertionFailure()\n                    }\n                }\n            )\n            .tint(action.appearance.isDestructive ? .themedNegative : nil)\n            .disabled(action.disabled)\n        } else if let action = action as? ActionGroup {\n            switch action.displayMode {\n            case .section:\n                SwiftUI.Section {\n                    iterateActions(actions: action.children)\n                }\n            case .compactSection:\n                ControlGroup {\n                    iterateActions(actions: action.children)\n                }\n                .controlGroupStyle(.compactMenu)\n            case .disclosure:\n                Menu {\n                    iterateActions(actions: action.children)\n                } label: {\n                    Label(action.appearance.label, systemImage: action.appearance.menuIcon)\n                }\n            case .popup:\n                Button(\n                    action.appearance.label,\n                    systemImage: action.appearance.menuIcon,\n                    role: action.appearance.isDestructive ? .destructive : nil,\n                    action: {\n                        if let popupModel {\n                            popupModel.showPopup(action)\n                        } else {\n                            assertionFailure()\n                        }\n                    }\n                )\n                .disabled(action.disabled)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func iterateActions(actions: [any Action]) -> some View {\n        ForEach(actions, id: \\.id) { action in\n            MenuButton(action: action)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/MessageView.swift",
    "content": "//\n//  MessageView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct MessageView<EmbeddedContent: View>: View {\n    @Environment(AppState.self) private var appState\n    @Environment(NavigationLayer.self) private var navigation\n    @Environment(\\.reportContext) private var reportContext\n    \n    @Setting(\\.menus_modActionGrouping) var moderatorActionGrouping\n    \n    let message: any Message\n    let notification: InboxNotification?\n    let embeddedContent: EmbeddedContent\n    \n    init(\n        message: any Message,\n        notification: InboxNotification?,\n        @ViewBuilder embeddedContent: () -> EmbeddedContent = { EmptyView() }\n    ) {\n        self.message = message\n        self.notification = notification\n        self.embeddedContent = embeddedContent()\n    }\n    \n    var body: some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            HStack {\n                FullyQualifiedLinkView(message.creator_, labelStyle: .small)\n                Spacer()\n                if let notification {\n                    Image(icon: message.isOwnMessage ? .lemmy.send : .lemmy.message)\n                        .symbolVariant(notification.read ? .none : .fill)\n                        .foregroundStyle(.themedAccent)\n                }\n                if let notification {\n                    EllipsisMenu(size: 24, notification: notification)\n                        .frame(height: 10)\n                } else if let reportContext {\n                    EllipsisMenu(size: 24, message: message, report: reportContext)\n                        .frame(height: 10)\n                }\n            }\n            if message.deleted {\n                Text(\"Message was deleted\")\n                    .italic()\n                    .foregroundStyle(.themedSecondary)\n            } else {\n                MarkdownWithLinkList(message.content)\n            }\n            Group {\n                if message.isOwnMessage {\n                    Text(\"Sent \\(message.created.getRelativeTime())\")\n                } else {\n                    Text(\"Received \\(message.created.getRelativeTime())\")\n                }\n            }\n            .font(.caption)\n            .foregroundStyle(.themedSecondary)\n            embeddedContent\n        }\n        .padding(.vertical, 2)\n        .padding(Constants.main.standardSpacing)\n        .clipped()\n        .background(.themedSecondaryGroupedBackground)\n        .contentShape(.rect)\n        .quickSwipes(message.swipeActions(notification: notification, appState: appState))\n        .clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n        .contextMenu(notification: notification, message: message, report: reportContext)\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        .onTapGesture {\n            if let otherPerson, message.api.canInteract(appState: appState) {\n                navigation.push(.messageFeed(otherPerson))\n            }\n        }\n    }\n    \n    var otherPerson: Person? {\n        message.isOwnMessage ? message.recipient_ : message.creator_\n    }\n    \n    @MainActor\n    func editMessage() {\n        if let otherPerson {\n            navigation.push(.messageFeed(otherPerson, focusTextField: true, editing: message))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ModlogButtonView.swift",
    "content": "//\n//  ModlogButtonView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-25.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ModlogButtonView: View {\n    let target: ModlogView.InitialTarget\n    \n    init(community: Community) {\n        self.target = .community(community)\n    }\n    \n    init(instance: Instance) {\n        self.target = .instance(instance)\n    }\n    \n    var body: some View {\n        NavigationLink(.modlog(target, targetPerson: nil, moderatorPerson: nil)) {\n            FormChevron {\n                Label {\n                    Text(\"Modlog\")\n                } icon: {\n                    Image(icon: .lemmy.modlog)\n                        .foregroundStyle(.themedSecondary)\n                }\n            }\n            .padding(.vertical, Constants.main.halfSpacing)\n            .padding(.horizontal, 15)\n            .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n            .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        }\n        .buttonStyle(.empty)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/MultiplatformView.swift",
    "content": "//\n//  MultiplatformView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-13.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct MultiplatformView<PhoneContent: View, PadContent: View>: View {\n    let phone: PhoneContent?\n    let pad: PadContent?\n    \n    init(@ViewBuilder phone: () -> PhoneContent, @ViewBuilder pad: () -> PadContent) {\n        if UIDevice.isPad {\n            self.phone = nil\n            self.pad = pad()\n        } else {\n            self.phone = phone()\n            self.pad = nil\n        }\n    }\n    \n    var body: some View {\n        if let phone {\n            phone\n        } else if let pad {\n            pad\n        } else {\n            Text(verbatim: \"MultiplatformView: Unsupported platform\")\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/LoginPage.swift",
    "content": "//\n//  LoginPage.swift\n//  Mlem\n//\n//  Created by Sjmarf on 13/05/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nenum LoginPage: Hashable {\n    case pickInstance\n    case instance(_ instance: Instance)\n    case reauth(_ account: UserAccount)\n    case totp(client: ApiClient, usernameOrEmail: String, password: String)\n    \n    @ViewBuilder\n    func view() -> some View {\n        switch self {\n        case .pickInstance:\n            LoginInstancePickerView()\n        case let .instance(instance):\n            LoginCredentialsView(instance: instance)\n        case let .reauth(account):\n            LoginCredentialsView(account: account)\n        case let .totp(client, usernameOrEmail, password):\n            LoginTotpView(client: client, usernameOrEmail: usernameOrEmail, password: password)\n        }\n    }\n    \n    static func == (lhs: LoginPage, rhs: LoginPage) -> Bool {\n        switch (lhs, rhs) {\n        case (.pickInstance, .pickInstance):\n            true\n        case let (.totp(url1, username1, password1), .totp(url2, username2, password2)):\n            url1 == url2 && username1 == username2 && password1 == password2\n        case let (.instance(instance1), .instance(instance2)):\n            instance1.actorId == instance2.actorId\n        case let (.reauth(user1), .reauth(user2)):\n            user1 == user2\n        default:\n            false\n        }\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        switch self {\n        case .pickInstance:\n            hasher.combine(\"pickInstance\")\n        case .totp:\n            hasher.combine(\"totp\")\n        case let .instance(instance):\n            hasher.combine(instance.actorId)\n        case let .reauth(user):\n            hasher.combine(user.actorId)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/NavigationLayer.swift",
    "content": "//\n//  NavigationLayer.swift\n//  Mlem\n//\n//  Created by Sjmarf on 28/04/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\nimport UniformTypeIdentifiers\n\n@Observable\nclass NavigationLayer: Identifiable {\n    var id: ObjectIdentifier { .init(self) }\n    \n    weak var model: NavigationModel?\n    var index: Int\n    \n    var root: NavigationPage\n    var path: [NavigationPage]\n    var hasNavigationStack: Bool\n    var isFullScreenCover: Bool\n    var canDisplayToasts: Bool\n\n    var rootViewPresentationDetent: PresentationDetent \n\n    // Used by ActionSheet\n    var rootChangePending: Bool = false\n    \n    init(\n        root: NavigationPage,\n        path: [NavigationPage] = [],\n        model: NavigationModel,\n        index: Int = -1,\n        hasNavigationStack: Bool = true,\n        isFullScreenCover: Bool = false,\n        canDisplayToasts: Bool = true\n    ) {\n        self.model = model\n        self.index = index\n        self.root = root\n        self.path = path\n        self.hasNavigationStack = hasNavigationStack\n        self.isFullScreenCover = isFullScreenCover\n        self.canDisplayToasts = canDisplayToasts\n        self.rootViewPresentationDetent = root.presentationDetentConfiguration?.default.presentationDetent() ?? .large\n    }\n    \n    @MainActor\n    func push(_ page: NavigationPage) {\n        if hasNavigationStack {\n            // This prevents keyboard animation glitches when navigating whilst the keyboard is open\n            UIApplication.shared.firstKeyWindow?.endEditing(true)\n            path.append(page)\n        } else {\n            openSheet(page)\n        }\n    }\n    \n    /// Replaces the current top level page with the given NavigationPage. Useful for redirecting from interim\n    /// pages that should not appear in navigation history.\n    @MainActor\n    func replace(_ page: NavigationPage) {\n        if hasNavigationStack {\n            // This prevents keyboard animation glitches when navigating whilst the keyboard is open\n            UIApplication.shared.firstKeyWindow?.endEditing(true)\n            if path.isEmpty {\n                root = page\n            } else {\n                path[path.count - 1] = page\n            }\n        } else {\n            openSheet(page)\n        }\n    }\n\n    @MainActor\n    func pop() {\n        if !path.isEmpty {\n            path.removeLast()\n        }\n        if path.isEmpty, index != -1 {\n            model?.closeSheets(aboveIndex: index)\n        }\n    }\n    \n    @MainActor\n    func dismissSheet() {\n        model?.closeSheets(aboveIndex: index)\n    }\n    \n    var isTopSheet: Bool {\n        isInsideSheet && index == (model?.layers.count ?? 0) - 1\n    }\n    \n    var isBottomLayer: Bool { index == -1 }\n    \n    var isToastDisplayer: Bool {\n        isInsideSheet\n            && canDisplayToasts\n            && model?.layers.last(where: { $0.canDisplayToasts }) === self\n    }\n    \n    func popToRoot() {\n        path.removeAll()\n    }\n    \n    var isAtRoot: Bool { path.isEmpty }\n    \n    /// Open a new sheet, optionally with navigation enabled. If `nil` is specified for `hasNavigationStack`, the value of `page.hasNavigationStack` will be used.\n    @MainActor\n    func openSheet(_ page: NavigationPage, hasNavigationStack: Bool? = nil) {\n        guard let model else {\n            assertionFailure()\n            return\n        }\n        rootChangePending = true\n        if case .actionSheet = root {\n            withAnimation {\n                if let detentsConfiguration = page.presentationDetentConfiguration {\n                    if detentsConfiguration.detents.contains(.large), self.rootViewPresentationDetent != .large {\n                        rootViewPresentationDetent = .large\n                    } else if detentsConfiguration.detents.contains(.medium), self.rootViewPresentationDetent != .medium {\n                        rootViewPresentationDetent = .medium\n                    }\n                } else {\n                    rootViewPresentationDetent = .large\n                }\n            } completion: {\n                withAnimation(.easeOut(duration: 0.3)) {\n                    self.root = page\n                    self.hasNavigationStack = page.hasNavigationStack\n                }\n            }\n        } else {\n            model.openSheet(\n                page,\n                hasNavigationStack: hasNavigationStack ?? page.hasNavigationStack\n            )\n        }\n    }\n    \n    /// Convenience proxy for showFullScreenCover. Opens the image viewer with the given URL and disables animations on the fullScreenCover.\n    @MainActor\n    func showImageViewer(url: URL) {\n        withoutAnimation {\n            self.showFullScreenCover(.imageViewer(url), hasNavigationStack: false)\n        }\n    }\n    \n    /// Open a new sheet, optionally with navigation enabled. If `nil` is specified for `hasNavigationStack`, the value of `page.hasNavigationStack` will be used.\n    @MainActor\n    func showFullScreenCover(_ page: NavigationPage, hasNavigationStack: Bool? = nil) {\n        model?.showFullScreenCover(\n            page,\n            hasNavigationStack: hasNavigationStack ?? page.hasNavigationStack\n        )\n    }\n    \n    @MainActor\n    func showPhotosPicker(for imageUploadManager: ImageUploadManager, api: ApiClient) {\n        model?.contentPickerTracker.photosPickerCallback = { photo in\n            Task {\n                do {\n                    guard let data = try await photo.loadTransferable(type: Data.self) else {\n                        throw ApiClientError.unsuccessful\n                    }\n                    guard let fileExtension = photo.supportedContentTypes.first?.preferredFilenameExtension else {\n                        throw ApiClientError.unsuccessful\n                    }\n                    if Settings.get(\\.behavior_confirmImageUploads) {\n                        self.openSheet(.confirmUpload(\n                            imageData: data,\n                            fileExtension: fileExtension,\n                            imageManager: imageUploadManager,\n                            uploadApi: api\n                        ))\n                    } else {\n                        try await imageUploadManager.upload(data: data, fileExtension: fileExtension, api: api)\n                    }\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n    }\n    \n    @MainActor\n    func showFilePicker(for imageUploadManager: ImageUploadManager, api: ApiClient) {\n        model?.contentPickerTracker.showingFilePicker = true\n        model?.contentPickerTracker.filePickerContentTypes = [.image]\n        model?.contentPickerTracker.filePickerCallback = { url in\n            Task {\n                do {\n                    guard url.startAccessingSecurityScopedResource() else {\n                        throw MlemError.cannotAccessSecurityScopedResource\n                    }\n                    let data = try Data(contentsOf: url)\n                    url.stopAccessingSecurityScopedResource()\n                    if Settings.get(\\.behavior_confirmImageUploads) {\n                        self.openSheet(.confirmUpload(\n                            imageData: data,\n                            fileExtension: url.pathExtension,\n                            imageManager: imageUploadManager,\n                            uploadApi: api\n                        ))\n                    } else {\n                        try await imageUploadManager.upload(data: data, fileExtension: url.pathExtension, api: api)\n                    }\n                } catch {\n                    url.stopAccessingSecurityScopedResource()\n                    handleError(error)\n                }\n            }\n        }\n    }\n    \n    @MainActor\n    func showFilePicker(types: [UTType], callback: @escaping (Data) async -> Void) {\n        model?.contentPickerTracker.showingFilePicker = true\n        model?.contentPickerTracker.filePickerContentTypes = types\n        model?.contentPickerTracker.filePickerCallback = { url in\n            Task {\n                do {\n                    guard url.startAccessingSecurityScopedResource() else {\n                        throw MlemError.cannotAccessSecurityScopedResource\n                    }\n                    let data = try Data(contentsOf: url)\n                    await callback(data)\n                    url.stopAccessingSecurityScopedResource()\n                } catch {\n                    url.stopAccessingSecurityScopedResource()\n                    handleError(error)\n                }\n            }\n        }\n    }\n    \n    @MainActor\n    func uploadImageFromClipboard(for imageUploadManager: ImageUploadManager, api: ApiClient) {\n        if UIPasteboard.general.hasImages, let content = UIPasteboard.general.image {\n            if let data = content.pngData() {\n                if Settings.get(\\.behavior_confirmImageUploads) {\n                    openSheet(.confirmUpload(\n                        imageData: data,\n                        fileExtension: \"png\",\n                        imageManager: imageUploadManager,\n                        uploadApi: api\n                    ))\n                } else {\n                    Task {\n                        do {\n                            try await imageUploadManager.upload(data: data, fileExtension: \"png\", api: api)\n                        } catch {\n                            handleError(error)\n                        }\n                    }\n                }\n            }\n        }\n    }\n    \n    var isInsideSheet: Bool { index != -1 }\n    \n    // Can be used inside of an `.onDisappear` to determine whether the disappearance was caused by the sheet closing\n    var isAlive: Bool { model != nil }\n    \n    var isImageViewer: Bool {\n        switch root {\n        case .imageViewer: true\n        default: false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/NavigationLayerView.swift",
    "content": "//\n//  NavigationLayerView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 28/04/2024.\n//\n\nimport SwiftUI\nimport SwiftUIIntrospect\nimport UIKit\n\nstruct NavigationLayerView: View {\n    @Setting(\\.appearance_interfaceStyle) var interfaceStyle\n\n    @State var layer: NavigationLayer\n\n    let hasSheetModifiers: Bool\n    var selectedDetent: Binding<PresentationDetent>?\n    \n    private let fullWidthGestureRecognizerDelegate: FullWidthGestureRecognizerDelegate = .init()\n    \n    var body: some View {\n        Group {\n            if layer.hasNavigationStack {\n                NavigationStack(path: $layer.path) {\n                    rootView()\n                        .environment(\\.isRootView, true)\n                        .navigationDestination(for: NavigationPage.self) {\n                            $0.view()\n                                .environment(\\.isRootView, false)\n                        }\n                }\n                .introspect(.navigationStack, on: .iOS(.v17, .v18)) { controller in\n                    // This is for the \"Swipe anywhere to navigate\" setting\n                    // https://stackoverflow.com/questions/20714595/extend-default-interactivepopgesturerecognizer-beyond-screen-edge\n                    guard\n                        let interactivePopGestureRecognizer = controller.interactivePopGestureRecognizer,\n                        let targets = interactivePopGestureRecognizer.value(forKey: \"targets\")\n                    else {\n                        return\n                    }\n                    \n                    let fullWidthBackGestureRecognizer = UIPanGestureRecognizer()\n                    fullWidthBackGestureRecognizer.setValue(targets, forKey: \"targets\")\n                    fullWidthGestureRecognizerDelegate.navigationController = controller\n                    fullWidthBackGestureRecognizer.delegate = fullWidthGestureRecognizerDelegate\n                    controller.view.addGestureRecognizer(fullWidthBackGestureRecognizer)\n                }\n               \n            } else {\n                rootView()\n                    .environment(\\.isRootView, true)\n            }\n        }\n        .overlay(alignment: .top) {\n            ToastOverlayView(\n                shouldDisplayNewToasts: layer.isToastDisplayer && hasSheetModifiers,\n                location: .top\n            )\n            .padding(.top, 8)\n            .ignoresSafeArea(edges: layer.isFullScreenCover ? [] : .top)\n        }\n        .overlay(alignment: .bottom) {\n            ToastOverlayView(\n                shouldDisplayNewToasts: layer.isToastDisplayer && hasSheetModifiers,\n                location: .bottom\n            )\n            .padding(.bottom, 8)\n        }\n        .modifier(HandleThreadiverseLinksModifier())\n        .environment(layer)\n        .preferredColorScheme(preferredColorScheme)\n    }\n    \n    @ViewBuilder\n    private func rootView() -> some View {\n        if hasSheetModifiers {\n            innerRootView().navigationSheetModifiers(for: layer)\n        } else {\n            innerRootView()\n        }\n    }\n\n    @ViewBuilder\n    private func innerRootView() -> some View {\n        if let selectedDetent {\n            layer.root.sheetView(selectedDetent: selectedDetent)\n        } else {\n            layer.root.view()\n        }\n    }\n    \n    private var preferredColorScheme: ColorScheme? {\n        @Setting(\\.appearance_palette) var colorPalette\n        let newStyle: UIUserInterfaceStyle = colorPalette.supportedModes != .unspecified ? colorPalette.supportedModes : interfaceStyle\n        \n        // The image viewer relies on having a concrete color scheme for the status bar color.\n        // Otherwise the status bar will \"flash\" when the sheet is dismissed\n        if layer.isImageViewer, newStyle == .unspecified {\n            return UIScreen.main.traitCollection.userInterfaceStyle == .dark ? .dark : .light\n        }\n        \n        return newStyle.colorScheme\n    }\n}\n\nprivate class FullWidthGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {\n    var navigationController: UINavigationController?\n\n    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {\n        guard Settings.get(\\.navigation_swipeAnywhere), !UIDevice.isIos26 else { return false }\n        let isSystemSwipeToBackEnabled = navigationController?.interactivePopGestureRecognizer?.isEnabled == true\n        let isThereStackedViewControllers = (navigationController?.viewControllers.count ?? 0) > 1\n        return isSystemSwipeToBackEnabled && isThereStackedViewControllers\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/NavigationModel.swift",
    "content": "//\n//  NavigationModel.swift\n//  Mlem\n//\n//  Created by Sjmarf on 27/04/2024.\n//\n\nimport PhotosUI\nimport SwiftUI\n\n@Observable\nclass NavigationModel {\n    static let main: NavigationModel = .init()\n    \n    private(set) var layers: [NavigationLayer] = .init()\n    \n    struct ShareInfo {\n        let url: URL\n        let activities: [ShareActivity]\n        \n        init(url: URL, activities: [ShareActivity]) {\n            self.url = url\n            self.activities = activities\n        }\n        \n        init(url: URL, actions: [BasicAction] = []) {\n            self.url = url\n            self.activities = actions.compactMap { action in\n                if let callback = action.callback {\n                    .init(appearance: action.appearance, performAction: callback)\n                } else {\n                    nil\n                }\n            }\n        }\n    }\n\n    @Observable\n    class ContentPickerTracker {\n        var photosPickerCallback: ((PhotosPickerItem) -> Void)?\n        \n        // This needs two values unlike `photosPickerCallback` because\n        // `fileImporter` sets `isPresented` to `false` before calling\n        // `onCompletion`, which makes it impossible to call the callback\n        // before setting it to `nil`.\n        var showingFilePicker: Bool = false\n        var filePickerCallback: ((URL) -> Void)?\n        var filePickerContentTypes: [UTType] = []\n    }\n    \n    var contentPickerTracker: ContentPickerTracker = .init()\n    \n    var mediaUrl: URL?\n    var shareInfo: ShareInfo?\n    var pendingOpenURL: URL?\n\n    @MainActor\n    private func openSheet(_ page: NavigationPage, hasNavigationStack: Bool? = nil, isFullScreenCover: Bool) {\n        guard Thread.isMainThread else {\n            assertionFailure()\n            ToastModel.main.add(.failure(\"Failed to open sheet\"))\n            return\n        }\n        \n        layers.append(\n            .init(\n                root: page,\n                model: self,\n                index: layers.count,\n                hasNavigationStack: hasNavigationStack ?? page.hasNavigationStack,\n                isFullScreenCover: isFullScreenCover,\n                canDisplayToasts: page.canDisplayToasts\n            )\n        )\n    }\n    \n    // Closes all sheets above and including the given index\n    @MainActor\n    func closeSheets(aboveIndex index: Int) {\n        for layer in layers.dropFirst(index) {\n            layer.model = nil\n        }\n        layers.removeLast(max(0, layers.count - index))\n    }\n    \n    @MainActor\n    func clear() {\n        layers.forEach { $0.model = nil }\n        layers = []\n    }\n    \n    @MainActor\n    func openSheet(_ page: NavigationPage, hasNavigationStack: Bool? = nil) {\n        openSheet(page, hasNavigationStack: hasNavigationStack, isFullScreenCover: false)\n    }\n    \n    @MainActor\n    func showFullScreenCover(_ page: NavigationPage, hasNavigationStack: Bool? = nil) {\n        openSheet(page, hasNavigationStack: hasNavigationStack, isFullScreenCover: true)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/NavigationPage+PresentationDetents.swift",
    "content": "//\n//  NavigationPage.swift\n//  Mlem\n//\n//  Created by Sjmarf on 27/04/2024.\n//\n\nimport ComponentViews\nimport SwiftUI\n\nextension NavigationPage {\n    // Return `nil` if you want the view itself to handle detents, rather than\n    // that it being handled by the navigation system. \n    var presentationDetentConfiguration: NavigationDetentConfiguration? {\n        switch self {\n        case .selectText: .only(.medium)\n        case .actionSheet: .init([.medium, .large], default: .medium)\n        case .externalApiInfo: .only(.medium)\n        case .rulesList: .init([.medium, .large], default: .medium)\n        case .quickSwitcher: .init([.medium, .large], default: .medium)\n        case .shareInstancePicker: .only(.fit)\n        case .remove, .denyApplication, .purge, .report, .editCommunity,\n            .createComment, .createPost, .ban: nil\n        default: .only(.large)\n        }\n    }\n\n    var fitDetentEnabled: Bool {\n        switch self {\n        case .shareInstancePicker: true\n        default: false\n        }\n    }\n}\n\nstruct NavigationDetentConfiguration {\n    enum Detent {\n        case medium, large, fit\n\n        func presentationDetent() -> PresentationDetent? {\n            switch self {\n            case .medium: .medium\n            case .large: .large\n            case .fit: nil\n            }\n        }\n    }\n\n    let detents: Set<Detent>\n    let `default`: Detent\n\n    init(_ detents: Set<Detent>, default default_: Detent) {\n        self.detents = detents\n        self.default = default_\n        assert(detents.contains(default_))\n    }\n\n    static func only(_ detent: Detent) -> Self {\n        .init([detent], default: detent)\n    }\n}\n\nprivate extension Set<NavigationDetentConfiguration.Detent> {\n    func presentationDetents() -> Set<PresentationDetent> {\n        .init(lazy.compactMap { $0.presentationDetent() })\n    }\n}\n\nextension View {\n    @ViewBuilder\n    func presentationDetents(\n        configuration: NavigationDetentConfiguration,\n        selection: Binding<PresentationDetent>\n    ) -> some View {\n        presentationDetentFitsContent(\n            fitDetentEnabled: configuration.detents.contains(.fit),\n            configuration.detents.presentationDetents(),\n            selection: selection\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/NavigationPage+View.swift",
    "content": "//\n//  NavigationPage+View.swift\n//  Mlem\n//\n//  Created by Sjmarf on 28/04/2024.\n//\n\nimport ComponentViews\nimport MlemBackend\nimport MlemMiddleware\nimport SwiftUI\n\nextension NavigationPage {\n\n    @ViewBuilder\n    func sheetView(selectedDetent: Binding<PresentationDetent>) -> some View {\n        if let presentationDetentConfiguration {\n            view()\n                .presentationDetents(configuration: presentationDetentConfiguration, selection: selectedDetent)\n        } else {\n            view()\n        }\n    }\n\n    // swiftlint:disable:next cyclomatic_complexity function_body_length\n    @ViewBuilder func view() -> some View {\n        switch self {\n        case .subscriptionList:\n            SubscriptionListView()\n        case let .selectText(string):\n            SelectTextView(text: string)\n        case let .shareInstancePicker(sharable):\n            ShareInstancePickerView(entity: sharable.wrappedValue)\n        case let .settings(page):\n            page.view()\n        case let .logIn(page):\n            page.view()\n        case let .signUp(instance):\n            SignUpView(instance: instance)\n        case .onboarding:\n            OnboardingView()\n        case let .feeds(listingType):\n            FeedsView(listingType: listingType)\n        case .savedFeed:\n            VisitAgainView(filter: .saved)\n        case .upvotedFeed:\n            VisitAgainView(filter: .upvoted)\n        case .topCommunities:\n            TopCommunitiesListView()\n        case .topPeople:\n            TopPeopleListView()\n        case .topInstances:\n            TopInstancesListView()\n        case let .communityStub(community):\n            CommunityStubResolutionPage(stub: community)\n        case let .community(community, visitContext):\n            CommunityView(community: community, visitContext: visitContext)\n        case .profile:\n            ProfileView()\n        case .inbox:\n            InboxView()\n        case .search:\n            SearchView()\n        case let .externalApiInfo(api: api, actorId: actorId):\n            ExternalApiInfoView(api: api, actorId: actorId)\n        case let .imageViewer(url):\n            ImageViewer(url: url)\n        case .quickSwitcher:\n            QuickSwitcherView()\n        case let .report(target, community):\n            ReportEditorView(target: target.wrappedValue, community: community)\n        case let .remove(target):\n            ContentRemovalEditorView(target: target.wrappedValue)\n        case let .purge(target):\n            ContentPurgeEditorView(target: target.wrappedValue)\n        case let .ban(person, isBannedFromCommunity: isBannedFromCommunity, shouldBan: shouldBan, community: community):\n            PersonBanEditorView(\n                person: person,\n                community: community,\n                isBannedFromCommunity: isBannedFromCommunity,\n                shouldBan: shouldBan\n            )\n        case let .post(post, scrollTargetedComment, communityContext, _):\n            // TODO: don't embed at all?\n            ExpandedPostView(post: post, tracker: nil, scrollTargetedComment: scrollTargetedComment) {\n                CrossPostListView(post: post)\n                    .padding(.horizontal, Constants.main.standardSpacing)\n            }\n            .environment(\\.communityContext, communityContext)\n        case let .postStub(post, _):\n            PostStubResolutionPage(stub: post)\n        case let .comment(comment, comments, showViewPostButton, exposeRemovedContent):\n            CommentPage(\n                comment: comment,\n                initialComments: comments,\n                showViewPostButton: showViewPostButton,\n                exposeRemovedContent: exposeRemovedContent\n            )\n        case let .commentStub(comment, comments, showViewPostButton, exposeRemovedContent):\n            CommentStubResolutionPage(\n                stub: comment,\n                comments: comments,\n                showViewPostButton: showViewPostButton,\n                exposeRemovedContent: exposeRemovedContent\n            )\n        case let .person(person, visitContext):\n            PersonView(person: person, visitContext: visitContext)\n        case let .personStub(person, visitContext):\n            PersonStubResolutionPage(stub: person, visitContext: visitContext)\n        case let .createComment(context, commentTreeTracker):\n            if let view = CommentEditorView(context: context, commentTreeTracker: commentTreeTracker) {\n                view\n            } else {\n                Text(verbatim: \"Error: No active UserAccount\")\n            }\n        case let .editComment(comment, context: context):\n            if let view = CommentEditorView(commentToEdit: comment, context: context) {\n                view\n            } else {\n                Text(verbatim: \"Error: No active UserAccount\")\n            }\n        case let .editCommunity(community):\n            CommunityDescriptionEditorView(community: community)\n        case let .editNote(person):\n            NoteEditorView(person: person)\n        case let .createPost(\n            community: community,\n            title: title,\n            content: content,\n            type: type,\n            nsfw: nsfw,\n            feedLoader: feedLoader\n        ):\n            if let view = PostEditorView(\n                community: community,\n                title: title,\n                content: content,\n                type: type,\n                nsfw: nsfw,\n                feedLoader: feedLoader.wrappedValue\n            ) {\n                view\n            } else {\n                Text(verbatim: \"Error: No active UserAccount\")\n            }\n        case let .editPost(post):\n            PostEditorView(postToEdit: post, community: nil)\n        case let .communityPicker(api: api, callback: callback):\n            SearchSheetView(api: api) { (community: Community, navigation: NavigationLayer) in\n                Button {\n                    callback.wrappedValue(community, navigation)\n                } label: {\n                    CommunityListRowBody(community, readout: .subscribers)\n                        .tint(.themedPrimary)\n                        .padding(.vertical, 6)\n                        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n                }\n            }\n        case let .personPicker(api: api, filter: filter, callback: callback):\n            SearchSheetView(api: api, filter: filter) { (person: Person, navigation: NavigationLayer) in\n                Button {\n                    callback.wrappedValue(person, navigation)\n                } label: {\n                    PersonListRowBody(person)\n                        .tint(.themedPrimary)\n                        .padding(.vertical, 6)\n                        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n                }\n            }\n        case let .instancePicker(callback: callback, requiredFeature: requiredFeature):\n            SearchSheetView { (instance: InstanceSummary, navigation: NavigationLayer) in\n                Button {\n                    callback.wrappedValue(instance, navigation)\n                } label: {\n                    InstanceListRowBody(instance)\n                        .tint(.themedPrimary)\n                        .padding(.vertical, 6)\n                        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n                }\n                .disabled(requiredFeature.map { !SiteSoftware(from: instance.software).supports($0) } ?? false)\n            } header: {\n                if requiredFeature != nil, requiredFeature != .signUp {\n                    Text(\"This feature is not available on all instances.\")\n                        .padding()\n                        .frame(maxWidth: .infinity, alignment: .leading)\n                        .background(.themedCaution.opacity(0.2), in: .rect(cornerRadius: Constants.main.standardSpacing))\n                        .foregroundStyle(.themedCaution)\n                        .padding(.horizontal, Constants.main.standardSpacing)\n                        .padding(.bottom, Constants.main.halfSpacing)\n                } else if let errorDetails = MlemStats.main.errorDetails {\n                    ErrorView(errorDetails)\n                        .padding(.top, 40)\n                }\n            }\n        case let .languagePicker(selectedLanguages: selectedLanguages, callback: callback):\n            LanguagePickerSheetView(selectedLanguages: selectedLanguages, callback: callback.wrappedValue)\n        case let .instance(instance, visitContext):\n            InstanceView(instance: instance, visitContext: visitContext)\n        case let .instanceStub(instance, targetPage):\n            InstanceStubResolutionPage(stub: instance, targetPage: targetPage.wrappedValue)\n        case let .instanceOpinionList(instance: instance, opinionType: opinionType, data: data):\n            FediseerOpinionListView(instance: instance, opinionType: opinionType, fediseerData: data)\n        case .fediseerInfo:\n            FediseerInfoView()\n        case let .instanceUptime(instance, uptimeData):\n            InstanceUptimeView(instance: instance, uptimeData: uptimeData)\n        case let .deleteAccount(account):\n            DeleteAccountView(account: account)\n        case let .bypassImageProxy(callback):\n            BypassProxyWarningSheet(callback: callback.wrappedValue)\n        case let .confirmUpload(imageData: imageData, fileExtension: fileExtension, imageManager: imageManager, uploadApi: uploadApi):\n            UploadConfirmationView(\n                imageData: imageData,\n                fileExtension: fileExtension,\n                imageManager: imageManager,\n                uploadApi: uploadApi\n            )\n        case let .rulesList(model, callback):\n            RulesPickerView(model: model.wrappedValue, callback: callback.wrappedValue)\n        case .blockList:\n            BlockListView()\n        case let .advancedSorting(sort):\n            AdvancedSortView(selectedSort: sort.wrappedValue)\n        case let .votesList(target):\n            VotesListView(target: target)\n        case let .messageFeed(person, messageContent: messageContent, focusTextField: focusTextField, editing: editing):\n            MessageFeedView(\n                person: person,\n                messageContent: messageContent,\n                focusTextField: focusTextField,\n                editing: editing?.wrappedValue\n            )\n        case let .modlog(target, targetPerson, moderatorPerson):\n            ModlogView(initialTarget: target, targetPerson: targetPerson, moderatorPerson: moderatorPerson)\n        case let .denyApplication(application):\n            RegistrationApplicationDenialEditorView(application: application)\n        case let .exportPostImage(post):\n            ExportablePostEditorView(post: post)\n        case let .exportCommentImage(comment, tracker):\n            ExportableCommentEditorView(comment: comment, commentTreeTracker: tracker)\n        case let .actionSheet(sections, environment, configuration):\n            ActionSheet(\n                sections: sections.wrappedValue,\n                environment: environment.wrappedValue,\n                configuration: configuration\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/NavigationPage.swift",
    "content": "//\n//  NavigationPage.swift\n//  Mlem\n//\n//  Created by Sjmarf on 27/04/2024.\n//\n\nimport Actions\nimport MlemBackend\nimport MlemMiddleware\nimport SwiftUI\n\n// swiftlint:disable file_length\n// swiftlint:disable:next type_body_length\nenum NavigationPage: Hashable {\n    case settings(_ page: SettingsPage = .root)\n    case logIn(_ page: LoginPage = .pickInstance)\n    case signUp(_ instance: Instance)\n    case onboarding\n    case feeds(_ selection: ListingType? = nil)\n    case savedFeed\n    case upvotedFeed\n    case topCommunities, topPeople, topInstances\n    case profile, inbox, search\n    case quickSwitcher\n    case post(\n        _ post: Post,\n        scrollTargetedComment: Comment? = nil,\n        communityContext: Community? = nil,\n        navigationNamespace: Namespace.ID? = nil\n    )\n    case postStub(_ post: PostStub, navigationNamespace: Namespace.ID? = nil)\n    case comment(\n        _ comment: Comment,\n        comments: [Comment]? = nil,\n        showViewPostButton: Bool = true,\n        exposeRemovedContent: Bool = false\n    )\n    case commentStub(\n        _ comment: CommentStub,\n        comments: [Comment]? = nil,\n        showViewPostButton: Bool = true,\n        exposeRemovedContent: Bool = false\n    )\n    case communityStub(\n        _ community: CommunityStub\n    )\n    case community(_ community: Community, visitContext: VisitHistory.VisitContext = .other)\n    case person(_ person: Person, visitContext: VisitHistory.VisitContext = .other)\n    case personStub(_ personStub: PersonStub, visitContext: VisitHistory.VisitContext = .other)\n    case instance(_ instance: Instance, visitContext: VisitHistory.VisitContext = .other)\n    case instanceStub(_ instanceStub: InstanceStub, targetPage: HashWrapper<(Instance) -> NavigationPage>)\n    case instanceOpinionList(instance: Instance, opinionType: FediseerOpinionType, data: FediseerData)\n    case messageFeed(_ person: Person, messageContent: String, focusTextField: Bool, editing: MessageHashWrapper?)\n    case fediseerInfo\n    case instanceUptime(_ instance: Instance, uptimeData: UptimeData)\n    case externalApiInfo(api: ApiClient, actorId: ActorIdentifier)\n    case imageViewer(_ url: URL)\n    case communityPicker(api: ApiClient?, callback: HashWrapper<(Community, NavigationLayer) -> Void>)\n    case personPicker(api: ApiClient?, filter: ListingType, callback: HashWrapper<(Person, NavigationLayer) -> Void>)\n    case instancePicker(callback: HashWrapper<(InstanceSummary, NavigationLayer) -> Void>, requiredFeature: Feature? = nil)\n    case languagePicker(selectedLanguages: Set<Locale.Language>, callback: HashWrapper<(Locale.Language) -> Void>)\n    case selectText(_ string: String)\n    case shareInstancePicker(_ sharable: SharableHashWrapper)\n    case subscriptionList\n    case createComment(_ context: CommentEditorView.Context, commentTreeTracker: CommentTreeTracker? = nil)\n    case editComment(_ comment: Comment, context: CommentEditorView.Context?)\n    case editCommunity(_ community: Community)\n    case editNote(_ person: Person)\n    case report(_ interactable: ReportableHashWrapper, community: Community? = nil)\n    case remove(_ removable: RemovableHashWrapper)\n    case purge(_ purgable: PurgableHashWrapper)\n    case ban(_ person: Person, isBannedFromCommunity: Bool, shouldBan: Bool, community: Community?)\n    case createPost(\n        community: Community?,\n        title: String,\n        content: String?,\n        type: PostType?,\n        nsfw: Bool,\n        feedLoader: HashWrapper<(any FeedLoading)?>\n    )\n    case editPost(_ post: Post)\n    case deleteAccount(_ account: UserAccount)\n    case bypassImageProxy(callback: HashWrapper<() -> Void>)\n    case confirmUpload(imageData: Data, fileExtension: String, imageManager: ImageUploadManager, uploadApi: ApiClient)\n    case rulesList(_ model: Profile2HashWrapper, callback: HashWrapper<(String) -> Void>)\n    case blockList\n    case advancedSorting(_ sort: HashWrapper<Binding<PostSortType>>)\n    case votesList(_ target: VotesListView.Target)\n    case modlog(ModlogView.InitialTarget, targetPerson: Person?, moderatorPerson: Person?)\n    case denyApplication(RegistrationApplication)\n    case exportPostImage(_ post: Post)\n    case exportCommentImage(_ comment: Comment, tracker: CommentTreeTracker?)\n\n    // If `configuration` is specified, show a \"customise\" button in the sheet for editing that configuration.\n    // Otherwise, no \"customise\" button is shown.\n    case actionSheet(\n        _ actions: HashWrapper<[ActionSheetSection]>,\n        environment: HashWrapper<EnvironmentValues>,\n        configuration: ContextMenuSettingsPage?\n    )\n\n    static func shareInstancePicker(_ sharable: any Sharable) -> NavigationPage {\n        shareInstancePicker(.init(wrappedValue: sharable))\n    }\n    \n    static func modlog(\n        community: Community,\n        targetPerson: Person? = nil,\n        moderatorPerson: Person? = nil\n    ) -> NavigationPage {\n        modlog(.community(community), targetPerson: targetPerson, moderatorPerson: moderatorPerson)\n    }\n    \n    static func modlog(\n        instance: Instance,\n        targetPerson: Person? = nil,\n        moderatorPerson: Person? = nil\n    ) -> NavigationPage {\n        modlog(.instance(instance), targetPerson: targetPerson, moderatorPerson: moderatorPerson)\n    }\n\n    static func modlog(\n        targetPerson: Person? = nil,\n        moderatorPerson: Person? = nil\n    ) -> NavigationPage {\n        modlog(.currentInstance, targetPerson: targetPerson, moderatorPerson: moderatorPerson)\n    }\n    \n    static func messageFeed(\n        _ person: Person,\n        messageContent: String = \"\",\n        focusTextField: Bool = false,\n        editing: (any Message1Providing)? = nil\n    ) -> NavigationPage {\n        var editingWrapper: MessageHashWrapper?\n        if let editing {\n            editingWrapper = .init(wrappedValue: editing)\n        }\n        return messageFeed(\n            person,\n            messageContent: messageContent,\n            focusTextField: focusTextField,\n            editing: editingWrapper\n        )\n    }\n    \n    static func instanceStub(_ stub: InstanceStub, visitContext: VisitHistory.VisitContext = .other) -> NavigationPage {\n        .instanceStub(stub, targetPage: .init(wrappedValue: { .instance($0, visitContext: visitContext) }))\n    }\n    \n    static func instanceStub(_ stub: InstanceStub, targetPage: @escaping (Instance) -> NavigationPage) -> NavigationPage {\n        .instanceStub(stub, targetPage: .init(wrappedValue: targetPage))\n    }\n    \n    static func hostInstance(\n        of entity: any ActorIdentifiable,\n        visitContext: VisitHistory.VisitContext = .other\n    ) -> NavigationPage {\n        if let entity = entity as? Person,\n           let instance = entity.instance.value_ {\n            return .instance(instance, visitContext: visitContext)\n        }\n        if let entity = entity as? Community,\n           let instance = entity.instance.value_ as? Instance {\n            return .instance(instance, visitContext: visitContext)\n        }\n        return .instanceStub(.init(api: AppState.main.firstApi, actorId: .instance(host: entity.actorId.host)))\n    }\n    \n    static func communityPicker(\n        api: ApiClient? = nil,\n        callback: @escaping (Community, NavigationLayer) -> Void\n    ) -> NavigationPage {\n        communityPicker(api: api, callback: .init(wrappedValue: callback))\n    }\n    \n    static func personPicker(\n        api: ApiClient? = nil,\n        filter: ListingType = .all,\n        callback: @escaping (Person, NavigationLayer) -> Void\n    ) -> NavigationPage {\n        personPicker(api: api, filter: filter, callback: .init(wrappedValue: callback))\n    }\n    \n    static func instancePicker(\n        callback: @escaping (InstanceSummary, NavigationLayer) -> Void,\n        requiredFeature: Feature? = nil\n    ) -> NavigationPage {\n        instancePicker(callback: .init(wrappedValue: callback), requiredFeature: requiredFeature)\n    }\n    \n    static func languagePicker(\n        selectedLanguages: Set<Locale.Language>,\n        callback: @escaping (Locale.Language) -> Void\n    ) -> NavigationPage {\n        languagePicker(selectedLanguages: selectedLanguages, callback: .init(wrappedValue: callback))\n    }\n    \n    static func signUp() -> NavigationPage {\n        .instancePicker(callback: { instance, navigation in\n            Task { @MainActor in\n                navigation.push(.signUp(instance.instanceStub))\n            }\n        }, requiredFeature: .signUp)\n    }\n    \n    static func signUp(_ stub: InstanceStub) -> NavigationPage {\n        .instanceStub(stub, targetPage: .init(wrappedValue: { .signUp($0) }))\n    }\n    \n    static func communityPicker(\n        api: ApiClient? = nil,\n        callback: @escaping (Community) -> Void\n    ) -> NavigationPage {\n        communityPicker(api: api, callback: .init(wrappedValue: { value, navigation in\n            Task { @MainActor in\n                navigation.dismissSheet()\n                callback(value)\n            }\n        }))\n    }\n    \n    static func personPicker(\n        api: ApiClient? = nil,\n        filter: ListingType = .all,\n        callback: @escaping (Person) -> Void\n    ) -> NavigationPage {\n        personPicker(api: api, filter: filter, callback: .init(wrappedValue: { value, navigation in\n            Task { @MainActor in\n                navigation.dismissSheet()\n                callback(value)\n            }\n        }))\n    }\n    \n    static func instancePicker(\n        callback: @escaping (InstanceSummary) -> Void,\n        requiredFeature: Feature? = nil\n    ) -> NavigationPage {\n        instancePicker(callback: .init(wrappedValue: { value, navigation in\n            Task { @MainActor in\n                navigation.dismissSheet()\n                callback(value)\n            }\n        }), requiredFeature: requiredFeature)\n    }\n    \n    static func createPost(\n        community: Community?,\n        title: String = \"\",\n        content: String? = nil,\n        type: PostType?,\n        nsfw: Bool = false,\n        feedLoader: (any FeedLoading)?\n    ) -> NavigationPage {\n        return createPost(\n            community: community,\n            title: title,\n            content: content,\n            type: type,\n            nsfw: nsfw,\n            feedLoader: .init(wrappedValue: feedLoader)\n        )\n    }\n\n    static func report(_ interactable: any ReportableProviding, community: Community?) -> NavigationPage {\n        return report(.init(wrappedValue: interactable), community: community)\n    }\n    \n    static func remove(_ interactable: any RemovableProviding) -> NavigationPage {\n        remove(.init(wrappedValue: interactable))\n    }\n    \n    static func purge(_ purgable: any PurgableProviding) -> NavigationPage {\n        purge(.init(wrappedValue: purgable))\n    }\n    \n    static func bypassImageProxyWarning(callback: @escaping () -> Void) -> NavigationPage {\n        bypassImageProxy(callback: .init(wrappedValue: callback))\n    }\n    \n    static func rulesList(_ model: any ProfileProviding, callback: @escaping (String) -> Void) -> NavigationPage {\n        rulesList(.init(wrappedValue: model), callback: .init(wrappedValue: callback))\n    }\n    \n    static func advancedSorting(_ sort: Binding<PostSortType>) -> NavigationPage {\n        advancedSorting(.init(wrappedValue: sort))\n    }\n\n    static func actionSheet(\n        _ actions: [ActionSheetSection],\n        environment: EnvironmentValues,\n        configuration: ReferenceWritableKeyPath<SettingsValues, some ContextMenuConfiguration>? = nil\n    ) -> NavigationPage {\n        actionSheet(\n            .init(wrappedValue: actions),\n            environment: .init(wrappedValue: environment),\n            configuration: configuration.map(ContextMenuSettingsPage.init)\n        )\n    }\n    \n    var hasNavigationStack: Bool {\n        switch self {\n        case .quickSwitcher, .report, .externalApiInfo, .selectText, .createComment,\n             .editComment, .createPost, .editPost, .denyApplication, .actionSheet:\n            false\n        default:\n            true\n        }\n    }\n    \n    var canDisplayToasts: Bool {\n        switch self {\n        case .quickSwitcher, .externalApiInfo, .selectText, .advancedSorting:\n            false\n        default:\n            true\n        }\n    }\n}\n\nstruct HashWrapper<Value>: Hashable, Identifiable {\n    let wrappedValue: Value\n    let id = UUID()\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(id)\n    }\n    \n    static func == (lhs: HashWrapper, rhs: HashWrapper) -> Bool {\n        lhs.id == rhs.id\n    }\n}\n\nstruct ReportableHashWrapper: Hashable {\n    var wrappedValue: any ReportableProviding\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(wrappedValue.hashValue)\n    }\n    \n    static func == (lhs: ReportableHashWrapper, rhs: ReportableHashWrapper) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n\nstruct SharableHashWrapper: Hashable {\n    var wrappedValue: any Sharable\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(wrappedValue.hashValue)\n    }\n    \n    static func == (lhs: SharableHashWrapper, rhs: SharableHashWrapper) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n\nstruct RemovableHashWrapper: Hashable {\n    var wrappedValue: any RemovableProviding\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(wrappedValue.hashValue)\n    }\n    \n    static func == (lhs: RemovableHashWrapper, rhs: RemovableHashWrapper) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n\nstruct PurgableHashWrapper: Hashable {\n    var wrappedValue: any PurgableProviding\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(wrappedValue.hashValue)\n    }\n    \n    static func == (lhs: PurgableHashWrapper, rhs: PurgableHashWrapper) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n\nstruct Profile2HashWrapper: Hashable {\n    var wrappedValue: any ProfileProviding\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(wrappedValue.actorId)\n    }\n    \n    static func == (lhs: Profile2HashWrapper, rhs: Profile2HashWrapper) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n\nstruct MessageHashWrapper: Hashable {\n    var wrappedValue: any Message1Providing\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(wrappedValue.actorId)\n    }\n    \n    static func == (lhs: MessageHashWrapper, rhs: MessageHashWrapper) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n\n// swiftlint:enable file_length\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/NavigationRootView.swift",
    "content": "//\n//  NavigationRoot.swift\n//  Mlem\n//\n//  Created by Sjmarf on 27/04/2024.\n//\n\nimport SwiftUI\n\nstruct NavigationSplitRootView: View {\n    @State var layer: NavigationLayer\n    let sidebar: NavigationPage\n    \n    @State var columnVisibility: NavigationSplitViewVisibility = .all\n    \n    init(sidebar: NavigationPage, root: NavigationPage) {\n        self._layer = .init(wrappedValue: .init(\n            root: UIDevice.isPad ? root : sidebar,\n            path: UIDevice.isPad ? [] : [root],\n            model: .main\n        ))\n        self.sidebar = sidebar\n        self._columnVisibility = .init(wrappedValue: Settings.get(\\.navigation_sidebarVisibleByDefault) ? .all : .detailOnly)\n    }\n    \n    var body: some View {\n        MultiplatformView(\n            phone: {\n                NavigationLayerView(layer: layer, hasSheetModifiers: false)\n            },\n            pad: {\n                NavigationSplitView(\n                    columnVisibility: $columnVisibility,\n                    sidebar: {\n                        sidebar.view()\n                    },\n                    detail: {\n                        NavigationLayerView(layer: layer, hasSheetModifiers: false)\n                            .id(layer.root)\n                    }\n                )\n                .modifier(HandleThreadiverseLinksModifier())\n            }\n        )\n        .environment(layer)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/NavigationSearchType.swift",
    "content": "//\n//  NavigationSearchType.swift\n//  Mlem\n//\n//  Created by Sjmarf on 27/06/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nenum NavigationSearchType {\n    case community\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/SettingsPage.swift",
    "content": "//\n//  SettingsPage.swift\n//  Mlem\n//\n//  Created by Sjmarf on 07/05/2024.\n//\n\nimport LemmyMarkdownUI\nimport SwiftUI\n\n// swiftlint:disable:next type_body_length\nenum SettingsPage: Hashable {\n    enum ContentActionType: Hashable {\n        case post, comment, inboxNotification, postReport, commentReport\n    }\n\n    enum SwipeActionSettingType: Hashable {\n        case post, comment, inboxNotification, postReport, commentReport, community\n    }\n\n    case root\n    case accounts, account\n    case profile, accountContent, accountAdvanced, accountSignIn, accountChangeEmail, accountLocal, accountChangePassword, accountLanguages\n    case general, privacy, safety, accessibility, sorting, filters\n    case zoomSlider\n    case defaultFeed, haptics, accountAgeVisibility\n    case privacyBypassImageProxy\n    case safetyBlurNsfw, safetyWarnings\n    case links, embedding\n    case imageViewer, imageViewerControls, imageViewerDismissSensitivity\n    case animatedAvatars\n    case externalLinks, sharingLinks, tappableLinks\n    case importExportSettings\n    case theme, icon\n    case post, comment, inbox, community, subscriptionList\n    case tabBar, longPressAction\n    case postThumbnail, postSubscriptionIndicator, postReadIndicator\n    case commentMaximumDepth, commentJumpButton\n    case inboxBadge\n    case about, advanced, developer, errorLog\n    case interactionBar(ContentActionType)\n    case swipeActions(SwipeActionSettingType)\n    case contextMenu(ContextMenuSettingsPage)\n    case postBarWidgetPicker(HashWrapper<Binding<PostBarConfiguration>>)\n    case commentBarWidgetPicker(HashWrapper<Binding<CommentBarConfiguration>>)\n    case replyBarWidgetPicker(HashWrapper<Binding<ReplyBarConfiguration>>)\n    case postReportBarWidgetPicker(HashWrapper<Binding<PostBarConfiguration>>)\n    case commentReportBarWidgetPicker(HashWrapper<Binding<CommentBarConfiguration>>)\n    case moderation\n    case modMailInteractionBar\n    case separateModeratorActions\n    case licences, document(Document)\n\n    static func contextMenu(_ keyPath: ReferenceWritableKeyPath<SettingsValues, some ContextMenuConfiguration>) -> Self {\n        .contextMenu(.init(keyPath))\n    }\n    \n    @ViewBuilder\n    // swiftlint:disable:next cyclomatic_complexity function_body_length\n    func view() -> some View {\n        switch self {\n        case .root:\n            SettingsView()\n        case .account:\n            AccountSettingsView()\n        case .profile:\n            if let person = AppState.main.firstPerson {\n                ProfileSettingsView(person: person)\n            } else {\n                Text(verbatim: \"Error: No active user account\")\n            }\n        case .accountContent:\n            AccountContentSettingsView()\n        case .accountSignIn:\n            AccountSignInSettingsView()\n        case .accountAdvanced:\n            AccountAdvancedSettingsView()\n        case .accountChangeEmail:\n            AccountEmailSettingsView()\n        case .accountChangePassword:\n            ChangePasswordView()\n        case .accountLanguages:\n            DiscussionLanguageSettingsView()\n        case .accountLocal:\n            AccountLocalSettingsView()\n        case .accounts:\n            AccountListSettingsView()\n        case .general:\n            GeneralSettingsView()\n        case .defaultFeed:\n            DefaultFeedSettingsView()\n        case .haptics:\n            HapticSettingsView()\n        case .accountAgeVisibility:\n            AccountAgeVisibilitySettingsView()\n        case .privacy:\n            PrivacySettingsView()\n        case .privacyBypassImageProxy:\n            PrivacyBypassImageProxySettingsView()\n        case .safety:\n            SafetySettingsView()\n        case .safetyBlurNsfw:\n            SafetyBlurNsfwSettingsView()\n        case .safetyWarnings:\n            SafetyWarningsSettingsView()\n        case .accessibility:\n            AccessibilitySettingsView()\n        case .zoomSlider:\n            ZoomSliderSettingsView()\n        case .importExportSettings:\n            ImportExportSettingsView()\n        case .advanced:\n            AdvancedSettingsView()\n        case .developer:\n            DeveloperSettingsView()\n        case .errorLog:\n            ErrorLogView()\n        case .about:\n            AboutMlemView()\n        case .theme:\n            ThemeSettingsView()\n        case .icon:\n            IconSettingsView()\n        case .post:\n            PostSettingsView()\n        case .community:\n            CommunitySettingsView()\n        case .postThumbnail:\n            PostThumbnailSettingsView()\n        case .postSubscriptionIndicator:\n            PostSubscriptionIndicatorSettingsView()\n        case .postReadIndicator:\n            PostReadIndicatorSettingsView()\n        case .comment:\n            CommentSettingsView()\n        case .commentMaximumDepth:\n            CommentMaximumDepthSettingsView()\n        case .commentJumpButton:\n            CommentJumpButtonSettingsView()\n        case .inbox:\n            InboxSettingsView()\n        case .links:\n            LinkSettingsView()\n        case .externalLinks:\n            ExternalLinkSettingsView()\n        case .sharingLinks:\n            SharingLinksSettingsView()\n        case .tappableLinks:\n            TappableLinksSettingsView()\n        case .embedding:\n            EmbeddingSettingsView()\n        case .animatedAvatars:\n            AnimatedAvatarSettingsView()\n        case .sorting:\n            SortingSettingsView()\n        case .filters:\n            FiltersSettingsView()\n        case .moderation:\n            ModeratorSettingsView()\n        case .modMailInteractionBar:\n            ModMailInteractionBarSettingsView()\n        case .separateModeratorActions:\n            ModeratorActionSeparationSettingsView()\n        case .subscriptionList:\n            SubscriptionListSettingsView()\n        case .tabBar:\n            TabBarSettingsView()\n        case .longPressAction:\n            LongPressActionSettingsView()\n        case .inboxBadge:\n            InboxBadgeSettingsView()\n        case .imageViewer:\n            ImageViewerSettingsView()\n        case .imageViewerControls:\n            ImageViewerShowControlsSettingsView()\n        case .imageViewerDismissSensitivity:\n            ImageViewerDismissSettingsView()\n        case let .swipeActions(type):\n            switch type {\n            case .post:\n                SwipeActionEditorView(\\.interactionBar_post, onApplyToAll: { configuration in\n                    Settings.mutate(\\.interactionBar_comment) {\n                        $0.applying(other: configuration, types: [.swipe])\n                    }\n                    Settings.mutate(\\.interactionBar_reply) {\n                        $0.applying(other: configuration, types: [.swipe])\n                    }\n                })\n            case .comment:\n                SwipeActionEditorView(\\.interactionBar_comment, onApplyToAll: { configuration in\n                    Settings.mutate(\\.interactionBar_post) {\n                        $0.applying(other: configuration, types: [.swipe])\n                    }\n                    Settings.mutate(\\.interactionBar_reply) {\n                        $0.applying(other: configuration, types: [.swipe])\n                    }\n                })\n            case .inboxNotification:\n                SwipeActionEditorView(\\.interactionBar_reply, onApplyToAll: { configuration in\n                    Settings.mutate(\\.interactionBar_post) {\n                        $0.applying(other: configuration, types: [.swipe])\n                    }\n                    Settings.mutate(\\.interactionBar_comment) {\n                        $0.applying(other: configuration, types: [.swipe])\n                    }\n                })\n            case .postReport:\n                SwipeActionEditorView(\\.interactionBar_postReport, onApplyToAll: { configuration in\n                    Settings.mutate(\\.interactionBar_commentReport) {\n                        $0.applying(other: configuration, types: [.swipe])\n                    }\n                })\n            case .commentReport:\n                SwipeActionEditorView(\\.interactionBar_commentReport, onApplyToAll: { configuration in\n                    Settings.mutate(\\.interactionBar_postReport) {\n                        $0.applying(other: configuration, types: [.swipe])\n                    }\n                })\n            case .community:\n                SwipeActionEditorView(\\.interactionBar_community)\n            }\n        case let .contextMenu(page):\n            page.view\n        case let .interactionBar(type):\n            switch type {\n            case .post:\n                InteractionBarEditorView(setting: \\.interactionBar_post, isReport: false)\n            case .comment:\n                InteractionBarEditorView(setting: \\.interactionBar_comment, isReport: false)\n            case .inboxNotification:\n                InteractionBarEditorView(setting: \\.interactionBar_reply, isReport: false)\n            case .postReport:\n                InteractionBarEditorView(setting: \\.interactionBar_postReport, isReport: true)\n            case .commentReport:\n                InteractionBarEditorView(setting: \\.interactionBar_commentReport, isReport: true)\n            }\n        case let .postBarWidgetPicker(configuration):\n            InteractionBarWidgetPickerView<PostBarConfiguration>(configuration: configuration.wrappedValue)\n        case let .commentBarWidgetPicker(configuration):\n            InteractionBarWidgetPickerView<CommentBarConfiguration>(configuration: configuration.wrappedValue)\n        case let .replyBarWidgetPicker(configuration):\n            InteractionBarWidgetPickerView<ReplyBarConfiguration>(configuration: configuration.wrappedValue)\n        case let .postReportBarWidgetPicker(configuration):\n            InteractionBarWidgetPickerView<PostBarConfiguration>(configuration: configuration.wrappedValue)\n        case let .commentReportBarWidgetPicker(configuration):\n            InteractionBarWidgetPickerView<CommentBarConfiguration>(configuration: configuration.wrappedValue)\n        case let .document(doc):\n            SimpleMarkdownPage(doc: doc)\n        case .licences:\n            Form {\n                ForEach(Document.allLicenses) { doc in\n                    NavigationLink(doc.title, destination: .settings(.document(doc)))\n                }\n            }\n        }\n    }\n    \n    static func postBarWidgetPicker(_ configuration: Binding<PostBarConfiguration>) -> SettingsPage {\n        .postBarWidgetPicker(.init(wrappedValue: configuration))\n    }\n    \n    static func commentBarWidgetPicker(_ configuration: Binding<CommentBarConfiguration>) -> SettingsPage {\n        .commentBarWidgetPicker(.init(wrappedValue: configuration))\n    }\n    \n    static func replyBarWidgetPicker(_ configuration: Binding<ReplyBarConfiguration>) -> SettingsPage {\n        .replyBarWidgetPicker(.init(wrappedValue: configuration))\n    }\n    \n    static func postReportBarWidgetPicker(_ configuration: Binding<PostBarConfiguration>) -> SettingsPage {\n        .postReportBarWidgetPicker(.init(wrappedValue: configuration))\n    }\n    \n    static func commentReportBarWidgetPicker(_ configuration: Binding<CommentBarConfiguration>) -> SettingsPage {\n        .commentReportBarWidgetPicker(.init(wrappedValue: configuration))\n    }\n}\n\nprivate struct SimpleMarkdownPage: View {\n    @Environment(\\.palette) var palette\n    \n    let doc: Document\n    \n    var body: some View {\n        ScrollView {\n            Markdown(doc.body, configuration: .default(palette: palette))\n                .padding(Constants.main.standardSpacing)\n        }\n        .background(.themedBackground)\n    }\n}\n\nstruct ContextMenuSettingsPage: Hashable {\n    let view: AnyView\n    let hash: Int\n\n    init(_ keyPath: ReferenceWritableKeyPath<SettingsValues, some ContextMenuConfiguration>) {\n        self.hash = keyPath.hashValue\n        self.view = AnyView(ContextMenuSettingsView(keyPath))\n    }\n\n    func hash(into hasher: inout Hasher) {\n        hasher.combine(hash)\n    }\n\n    static func == (lhs: Self, rhs: Self) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Navigation/View+NavigationSheetModifiers.swift",
    "content": "//\n//  NavigationPage+View.swift\n//  Mlem\n//\n//  Created by Sjmarf on 28/04/2024.\n//\n\nimport SwiftUI\nimport Theming\n\nprivate struct NavigationSheetModifier: ViewModifier {\n    @Setting(\\.appearance_palette) var colorPalette\n    \n    let nextLayer: NavigationLayer?\n    let contentPickerTracker_: () -> NavigationModel.ContentPickerTracker?\n    let isTopSheet: Bool\n    \n    @Binding var shareInfo: NavigationModel.ShareInfo?\n    \n    init(\n        nextLayer: NavigationLayer?,\n        isTopSheet: Bool,\n        shareInfo: Binding<NavigationModel.ShareInfo?>,\n        // This tomfoolery exists to prevent this view being subject to NavigationModel view updates, which caused #1492\n        contentPickerTracker: @escaping () -> NavigationModel.ContentPickerTracker?\n    ) {\n        self.nextLayer = nextLayer\n        self.isTopSheet = isTopSheet\n        self._shareInfo = shareInfo\n        self.contentPickerTracker_ = contentPickerTracker\n    }\n    \n    // DO NOT access this in the view body; see #1492\n    var contentPickerTracker: NavigationModel.ContentPickerTracker? {\n        contentPickerTracker_()\n    }\n    \n    // swiftlint:disable:next function_body_length\n    func body(content: Content) -> some View {\n        content\n            // https://stackoverflow.com/questions/69693871/how-to-open-share-sheet-from-presented-sheet\n            .background(SharingViewController(\n                isPresenting: Binding(get: { shareInfo != nil && isTopSheet }, set: { if !$0 { shareInfo = nil }})\n            ) { activityViewController }\n            )\n            .sheet(item: Binding(\n                get: {\n                    if let nextLayer, !nextLayer.isFullScreenCover { nextLayer } else { nil }\n                },\n                set: { if $0 == nil { closeSheet() } }\n            )) { layer in\n                NavigationLayerView(\n                    layer: layer,\n                    hasSheetModifiers: true,\n                    selectedDetent: Binding(\n                        get: { layer.rootViewPresentationDetent },\n                        set: { layer.rootViewPresentationDetent = $0 }\n                    )\n                )\n            }\n            .fullScreenCover(item: Binding(\n                get: {\n                    if let nextLayer, nextLayer.isFullScreenCover { nextLayer } else { nil }\n                },\n                set: { if $0 == nil { closeSheet() } }\n            )) { layer in\n                NavigationLayerView(layer: layer, hasSheetModifiers: true)\n            }\n            .photosPicker(\n                isPresented: .init(\n                    get: { nextLayer == nil && contentPickerTracker?.photosPickerCallback != nil },\n                    set: { contentPickerTracker?.photosPickerCallback = $0 ? contentPickerTracker?.photosPickerCallback : nil }\n                ),\n                selection: .init(get: { nil }, set: { photo in\n                    if let photo {\n                        contentPickerTracker?.photosPickerCallback?(photo)\n                        contentPickerTracker?.photosPickerCallback = nil\n                    }\n                }),\n                matching: .images\n            )\n            .fileImporter(\n                isPresented: .init(\n                    get: { nextLayer == nil && (contentPickerTracker?.showingFilePicker ?? false) },\n                    set: { contentPickerTracker?.showingFilePicker = $0 }\n                ),\n                allowedContentTypes: contentPickerTracker?.filePickerContentTypes ?? [],\n                onCompletion: { result in\n                    do {\n                        try contentPickerTracker?.filePickerCallback?(result.get())\n                    } catch {\n                        handleError(error)\n                    }\n                }\n            )\n            .accentColor(ThemedColor.themedAccent.resolve(with: colorPalette.palette)) // deprecated, but .tint colors menu buttons\n    }\n    \n    var activityViewController: UIActivityViewController {\n        let activityView = UIActivityViewController(\n            activityItems: [shareInfo?.url ?? URL(string: \"www.apple.com\")!],\n            applicationActivities: shareInfo?.activities\n        )\n        \n        if UIDevice.isPad {\n            activityView.popoverPresentationController?.sourceView = UIView()\n        }\n        \n        activityView.completionWithItemsHandler = { _, _, _, _ in\n            shareInfo = nil\n        }\n        return activityView\n    }\n    \n    func closeSheet() {\n        if let nextLayer, let model = nextLayer.model {\n            model.closeSheets(aboveIndex: nextLayer.index)\n        }\n    }\n}\n\nprivate struct ComputeNextLayerModifier: ViewModifier {\n    let layer: NavigationLayer\n    \n    // This exists to prevent the view from being subject to NavigationModel state updates, which caused #1492\n    @State var nextLayer: NavigationLayer?\n    \n    func body(content: Content) -> some View {\n        Group {\n            content.navigationSheetModifiers(\n                nextLayer: nextLayer,\n                isTopSheet: layer.isTopSheet,\n                shareInfo: .init(get: { layer.model?.shareInfo }, set: { layer.model?.shareInfo = $0 }),\n                contentPickerTracker: layer.model?.contentPickerTracker\n            )\n        }.onChange(of: computeNextLayer()?.id, initial: true) {\n            nextLayer = computeNextLayer()\n        }\n    }\n    \n    func computeNextLayer() -> NavigationLayer? {\n        if let model = layer.model {\n            (layer.index < model.layers.count - 1) ? model.layers[layer.index + 1] : nil\n        } else {\n            nil\n        }\n    }\n}\n\nextension View {\n    @ViewBuilder func navigationSheetModifiers(for layer: NavigationLayer) -> some View {\n        modifier(ComputeNextLayerModifier(layer: layer))\n    }\n        \n    @ViewBuilder func navigationSheetModifiers(\n        nextLayer: NavigationLayer?,\n        isTopSheet: Bool,\n        shareInfo: Binding<NavigationModel.ShareInfo?>,\n        contentPickerTracker: @autoclosure @escaping () -> NavigationModel.ContentPickerTracker?\n    ) -> some View {\n        modifier(NavigationSheetModifier(\n            nextLayer: nextLayer,\n            isTopSheet: isTopSheet,\n            shareInfo: shareInfo,\n            contentPickerTracker: contentPickerTracker\n        ))\n    }\n}\n\nprivate struct SharingViewController: UIViewControllerRepresentable {\n    @Binding var isPresenting: Bool\n    var content: () -> UIViewController\n\n    func makeUIViewController(context: Context) -> UIViewController {\n        UIViewController()\n    }\n\n    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {\n        if isPresenting {\n            uiViewController.present(content(), animated: true, completion: { isPresenting = false })\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Palette Components/Button.swift",
    "content": "//\n//  Button.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-09-06.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct PaletteButton: ButtonStyle {\n    @Environment(\\.isEnabled) var isEnabled\n    \n    func makeBody(configuration: Configuration) -> some View {\n        Group {\n            if isEnabled {\n                configuration.label\n                    .foregroundStyle(.tint)\n            } else {\n                configuration.label\n                    .foregroundStyle(.themedSecondary)\n            }\n        }\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .contentShape(.rect)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Palette Components/Divider.swift",
    "content": "//\n//  Divider.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-30.\n//\n\nimport Foundation\nimport SwiftUI\n\n/// Divider() that colors itself appropriately to the palette.\n/// DO NOT use in UIKit environments!\nstruct Divider: View {\n    var body: some View {\n        SwiftUI.Divider()\n            .hidden()\n            .overlay(.themedDivider)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Palette Components/Form.swift",
    "content": "//\n//  Form.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-30.\n//\n\nimport Foundation\nimport SwiftUI\nimport Theming\n\n/// Identical to Form, but respects Palette\nstruct Form<Content: View>: View {\n    @Environment(\\.palette) var palette\n    \n    @ViewBuilder let content: () -> Content\n    \n    let tint: ThemedColor\n    \n    init(tint: ThemedColor = .themedAccent, @ViewBuilder content: @escaping () -> Content) {\n        self.tint = tint\n        self.content = content\n    }\n    \n    var body: some View {\n        SwiftUI.Form {\n            content()\n                .foregroundStyle(.themedPrimary)\n                .listRowBackground(palette.groupedBackground.secondary)\n                .buttonStyle(PaletteButton())\n        }\n        .tint(tint)\n        .listStyle(.insetGrouped)\n        .scrollContentBackground(.hidden)\n        .background(.themedGroupedBackground)\n        .themedGroupedBackground()\n        .shadow(color: palette.label.primary.opacity(palette.bordered ? 0.4 : 0.0), radius: 1)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Palette Components/Section.swift",
    "content": "//\n//  Section.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-30.\n//\n\nimport Foundation\nimport SwiftUI\n\nstruct Section<Parent: View, Content: View, Footer: View>: View {\n    @ViewBuilder let header: () -> Parent\n    @ViewBuilder let content: () -> Content\n    @ViewBuilder let footer: () -> Footer\n\n    init(\n        @ViewBuilder content: @escaping () -> Content,\n        @ViewBuilder header: @escaping () -> Parent = { EmptyView() },\n        @ViewBuilder footer: @escaping () -> Footer = { EmptyView() }\n    ) {\n        self.header = header\n        self.content = content\n        self.footer = footer\n    }\n\n    init(\n        _ header: LocalizedStringResource,\n        @ViewBuilder content: @escaping () -> Content,\n        @ViewBuilder footer: @escaping () -> Footer = { EmptyView() }\n    ) where Parent == Text {\n        self.header = { Text(header) }\n        self.content = content\n        self.footer = footer\n    }\n    \n    @_disfavoredOverload\n    init(\n        _ header: String,\n        @ViewBuilder content: @escaping () -> Content,\n        @ViewBuilder footer: @escaping () -> Footer = { EmptyView() }\n    ) where Parent == Text {\n        self.header = { Text(header) }\n        self.content = content\n        self.footer = footer\n    }\n\n    var body: some View {\n        SwiftUI.Section(\n            content: content,\n            header: { header().foregroundStyle(.themedSecondary) },\n            footer: { footer().foregroundStyle(.themedSecondary) }\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/PersonContentGridView+FeedLoaderType.swift",
    "content": "//\n//  PersonContentGridView+FeedLoaderType.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-29.\n//  \n\nimport Foundation\nimport MlemMiddleware\n\nextension PersonContentGridView {\n    enum FeedLoaderType {\n        case standard(StandardFeedLoader<PersonContent>, contentType: PersonContentType)\n        case singleSourceMixed(SingleSourceMixedFeedLoader, contentType: PersonContentType)\n\n        var items: [PersonContent] {\n            switch self {\n            case let .standard(feedLoader, _): feedLoader.items\n            case let .singleSourceMixed(feedLoader, contentType): feedLoader.itemsForType(contentType)\n            }\n        }\n        \n        var loadingState: FeedLoadingState {\n            switch self {\n            case let .singleSourceMixed(feedLoader, contentType): feedLoader.loadingStateForType(contentType)\n            default: feedLoading.loadingState\n            }\n        }\n        \n        var feedLoading: any FeedLoading {\n            switch self {\n            case let .standard(feedLoader, _): feedLoader\n            case let .singleSourceMixed(feedLoader, _): feedLoader\n            }\n        }\n        \n        func loadIfThreshold(_ item: PersonContent) throws {\n            switch self {\n            case let .standard(feedLoader, _): try feedLoader.loadIfThreshold(item)\n            case let .singleSourceMixed(feedLoader, contentType):\n                try feedLoader.loadIfThreshold(item, asChild: contentType != .all)\n            }\n        }\n        \n        var type: PersonContentType {\n            switch self {\n            case let .standard(_, type): type\n            case let .singleSourceMixed(_, type): type\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/PersonContentGridView.swift",
    "content": "//\n//  PersonContentGridView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-07-18.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nenum PersonContentType: CaseIterable, Identifiable {\n    case all, posts, comments\n    \n    var id: Self { self }\n    \n    var label: LocalizedStringResource {\n        switch self {\n        case .all: \"All\"\n        case .posts: \"Posts\"\n        case .comments: \"Comments\"\n        }\n    }\n}\n\nstruct PersonContentGridView: View {\n    @Environment(AppState.self) var appState\n    @Setting(\\.post_size) var postSize\n    @Setting(\\.behavior_infiniteScroll) var infiniteScroll\n    \n    @State var columns: [GridItem] = [GridItem(.flexible())]\n    @State var frameWidth: CGFloat = .zero\n    @State var errorDetails: ErrorDetails?\n    \n    var feedLoader: FeedLoaderType\n    \n    var body: some View {\n        content\n            .loadFeed(feedLoader.feedLoading, errorDetails: $errorDetails)\n            .widthReader(width: $frameWidth)\n            .environment(\\.parentFrameWidth, frameWidth)\n            .onChange(of: postSize, initial: true) { _, newValue in\n                if newValue.tiled {\n                    // leading/trailing alignment makes them want to stick to each other, allowing the Constants.main.halfSpacing padding applied below\n                    // to push them apart by a sum of Constants.main.standardSpacing\n                    columns = [\n                        GridItem(.flexible(), spacing: 0, alignment: .trailing),\n                        GridItem(.flexible(), spacing: 0, alignment: .leading)\n                    ]\n                } else if columns.count > 1 {\n                    // Only trigger if not already 1 column to avoid causing unnecessary view update\n                    columns = [GridItem(.flexible())]\n                }\n            }\n            .toolbar { FeedToolbarOptions() }\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        let items = feedLoader.items\n        VStack(spacing: 0) {\n            LazyVGrid(columns: columns, spacing: spacing) {\n                ForEach(items, id: \\.hashValue) { item in\n                    if !item.shouldHideInFeed {\n                        personContentItem(item)\n                            .buttonStyle(.empty)\n                            .padding(.horizontal, postSize.tiled ? Constants.main.halfSpacing : 10)\n                            .onAppear {\n                                do {\n                                    try feedLoader.loadIfThreshold(item)\n                                } catch {\n                                    // TODO: is postFeedLoader.loadIfThreshold throws 400, this line is not executed\n                                    handleError(error)\n                                }\n                            }\n                    }\n                }\n            }\n            .quickSwipeCornerRadius(postSize.cornerRadius)\n            .quickSwipeIconSize(postSize.quickSwipeIconSize)\n            .quickSwipeThresholds(postSize.quickSwipeThresholds)\n            .animation(.easeOut(duration: 0.1), value: items.isEmpty)\n            if let errorDetails {\n                ErrorView(errorDetails)\n            } else {\n                EndOfFeedView(loadingState: feedLoader.loadingState, viewType: .hobbit)\n            }\n        }\n    }\n    \n    var spacing: CGFloat {\n        switch feedLoader.type {\n        case .all, .comments:\n            postSize.sectionSpacing\n        case .posts:\n            postSize.sectionSpacing\n        }\n    }\n    \n    @ViewBuilder\n    func personContentItem(_ personContent: PersonContent) -> some View {\n        switch personContent.wrappedValue {\n        case let .post(post):\n            NavigationLink(.post(post)) {\n                FeedPostView(post: post)\n            }\n        case let .comment(comment):\n            NavigationLink(.comment(comment)) {\n                FeedCommentView(comment: comment)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/PostEllipsisMenus.swift",
    "content": "//\n//  PostEllipsisMenus.swift\n//  Mlem\n//\n//  Created by Sjmarf on 01/10/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\n/// Ellipsis menu for a post appearing in a larger view context. Posts appearing on their own page (i.e., ExpandedPostView) should\n/// place their ellipsis menu in the toolbar.\nstruct PostEllipsisMenus: View {\n    @Environment(AppState.self) private var appState\n    @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker?\n    @Environment(NavigationLayer.self) private var navigation\n    @Environment(\\.reportContext) private var reportContext: Report?\n    \n    @Setting(\\.interactionBar_post) var postInteractionBar\n    @Setting(\\.menus_modActionGrouping) var moderatorActionGrouping\n\n    // This @State is necessary!\n    @State var post: Post\n    \n    var size: CGFloat = 24\n    \n    var body: some View {\n        HStack {\n            if moderatorActionGrouping == .separateMenu {\n                if post.canModerate {\n                    EllipsisMenu(\n                        icon: .lemmy.moderation,\n                        size: size,\n                        post: post,\n                        type: [.moderator]\n                    )\n                }\n                EllipsisMenu(size: size, post: post, type: [.basic])\n            } else {\n                EllipsisMenu(size: size, post: post, type: [.basic, .moderator])\n            }\n        }\n        .environment(\\.communityContext, post.community.value)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/PostGridView.swift",
    "content": "//\n//  PostFeedView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-29.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\n/// Renders the content of a given StandardPostFeedLoader and adds a toolbar menu with the standard post feed controls. Additional toolbar actions\n/// should be handled by using ToolbarItemGroup(placement: .secondaryAction) on the parent view.\n/// This view handles:\n/// - Post layout\n/// - Loading\n/// - Default toolbar menu actions (show/hide read, post size)\n/// Scrolling, handling feed type changes, header, footer, etc. should be handled by the parent view\nstruct PostGridView: View {\n    @Setting(\\.post_size) var postSize\n    @Setting(\\.feed_showRead) var showRead\n    @Setting(\\.behavior_infiniteScroll) var infiniteScroll\n    @Setting(\\.post_allowMultipleColumns) var allowMultipleColumns\n    \n    @Environment(FiltersTracker.self) var filtersTracker\n    \n    @Environment(\\.communityContext) var communityContext\n    \n    @State var frameWidth: CGFloat = .zero\n    @State var bottomAppearedPostIndex: Int = -1\n    \n    @State var isWideEnoughForTwoColumns: Bool = false\n    \n    @Namespace var navigationNamespace\n    \n    let postFeedLoader: CorePostFeedLoader\n    \n    let alwaysShowRead: Bool\n\n    init(postFeedLoader: CorePostFeedLoader, alwaysShowRead: Bool = false) {\n        self.postFeedLoader = postFeedLoader\n        self.alwaysShowRead = alwaysShowRead\n    }\n    \n    var body: some View {\n        content\n            .widthReader(width: $frameWidth)\n            .environment(\\.parentFrameWidth, frameWidth)\n            .loadFeed(postFeedLoader)\n            .task(id: showRead) {\n                do {\n                    if showRead || alwaysShowRead {\n                        try await postFeedLoader.deactivateFilter(.read)\n                    } else {\n                        try await postFeedLoader.activateFilter(.read)\n                    }\n                } catch {\n                    handleError(error)\n                }\n            }\n            .onDisappear {\n                if let api = postFeedLoader.items.first?.api {\n                    Task {\n                        do {\n                            try await api.flushPostReadQueue()\n                        } catch {\n                            handleError(error)\n                        }\n                    }\n                }\n            }\n            .toolbar { FeedToolbarOptions() }\n    }\n    \n    var content: some View {\n        VStack(spacing: 0) {\n            GeometryReader { geometry in\n                Spacer()\n                    .onChange(of: geometry.size.width, initial: true) {\n                        let newVal = geometry.size.width > 700\n                        if isWideEnoughForTwoColumns != newVal { // Avoid unnecessary view update\n                            isWideEnoughForTwoColumns = newVal\n                        }\n                    }\n            }\n            .frame(height: 0)\n            let columns = columns\n            LazyVGrid(columns: columns, spacing: postSize.sectionSpacing) {\n                ForEach(Array(postFeedLoader.items.enumerated()), id: \\.element.hashValue) { index, post in\n                    if !post.shouldHideInFeed {\n                        NavigationLink(.post(post, communityContext: communityContext, navigationNamespace: navigationNamespace)) {\n                            FeedPostView(post: post, requireConsistentHeight: columns.count != 1)\n                                .matchedTransitionSource_(id: \"post\\(post.actorId)\", in: navigationNamespace)\n                        }\n                        .buttonStyle(.empty)\n                        .padding(.horizontal, postInnerPadding)\n                        .markReadOnScroll(\n                            index: index,\n                            post: post,\n                            postFeedLoader: postFeedLoader, bottomAppearedItemIndex: $bottomAppearedPostIndex\n                        )\n                        .onAppear {\n                            if infiniteScroll {\n                                do {\n                                    try postFeedLoader.loadIfThreshold(post)\n                                } catch {\n                                    // TODO: if postFeedLoader.loadIfThreshold throws 400, this line is not executed\n                                    handleError(error)\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            .quickSwipeCornerRadius(postSize.cornerRadius)\n            .quickSwipeIconSize(postSize.quickSwipeIconSize)\n            .quickSwipeThresholds(postSize.quickSwipeThresholds)\n            .padding(.horizontal, postSize.tiled || columns.count == 1 ? 0 : Constants.main.halfSpacing)\n            .animation(.easeOut(duration: 0.1), value: postFeedLoader.items.isEmpty)\n            EndOfFeedView(feedLoader: postFeedLoader, viewType: .hobbit)\n        }\n    }\n    \n    var postInnerPadding: CGFloat {\n        if columns.count == 1 {\n            Constants.main.standardSpacing\n        } else {\n            Constants.main.standardSpacing / (postSize == .compact ? 4 : 2)\n        }\n    }\n    \n    var columns: [GridItem] {\n        if postSize.tiled || (postSize != .large && isWideEnoughForTwoColumns), allowMultipleColumns {\n            // leading/trailing alignment makes them want to stick to each other, allowing the Constants.main.halfSpacing padding applied below\n            // to push them apart by a sum of Constants.main.standardSpacing\n            \n            // Avoid causing unnecessary view update\n            return [\n                GridItem(.flexible(), spacing: 0, alignment: .trailing),\n                GridItem(.flexible(), spacing: 0, alignment: .leading)\n            ]\n        } else {\n            // Only trigger if not already 1 column to avoid causing unnecessary view update\n            return [GridItem(.flexible())]\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ProfileDateView.swift",
    "content": "//\n//  ProfileDateView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 31/05/2024.\n//\n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct ProfileDateView: View {\n    var profilable: any ProfileProviding\n    \n    @ViewBuilder\n    var body: some View {\n        if let created = profilable.profileCreated {\n            Label(format(created), icon: icon)\n                .symbolVariant(profilable.createdRecently || profilable.isCakeDay ? .fill : .none)\n                .foregroundStyle(color)\n                .font(.footnote)\n        }\n    }\n    \n    var color: ThemedColor {\n        if profilable.createdRecently {\n            .themedColorfulAccent(3)\n        } else if profilable.isCakeDay {\n            .themedColorfulAccent(1)\n        } else {\n            .themedSecondary\n        }\n    }\n    \n    var icon: Icon {\n        profilable.createdRecently ? .lemmy.newAccountFlair : .lemmy.cakeDay\n    }\n    \n    /// Returns a `String` with the cake date and a custom message depending to if today is cake day or not.\n    /// If the current day is not the cake day, forges a `String` with relative time between the cake date and today.\n    /// In that case the result string is in *full* unit style so as to improve vocalization of the date with *Voice Over¨.\n    /// - Parameter date: the date to process, here the profile creation day\n    /// - Returns String: The enriched string to add in the profile\n    func format(_ date: Date) -> String {\n        if profilable.isCakeDay {\n            // It's possible for it to be a user's cake day without their account age quite being 365 days.\n            // To account for this we subtrat 1 day from the start date, to push it over the 1 year mark.\n            let startDate = date.addingTimeInterval(-60 * 60 * 24)\n            let components = Calendar.current.dateComponents([.year], from: startDate, to: .now)\n            return \"\\(date.dateString), \" + String(localized: \"\\(components.year ?? 0) years ago today!\")\n        }\n        return \"\\(date.dateString), \\(date.getRelativeTime(unitsStyle: .full))\"\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ReadCheck.swift",
    "content": "//\n//  ReadCheck.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-12-26.\n//\n\nimport Icons\nimport SwiftUI\nimport Theming\nimport MlemMiddleware\n\nstruct ReadCheck: View {\n    let dimension: CGFloat\n    let read: ExpectedValue<Bool>\n    \n    init(read: ExpectedValue<Bool>, tiled: Bool = false) {\n        self.read = read\n        self.dimension = tiled ? 10 : 12\n    }\n    \n    var body: some View {\n        ExpectedView(read) { read in\n            content(read: read)\n        } placeholder: {\n            ProgressView()\n        }\n    }\n    \n    @ViewBuilder\n    func content(read: Bool) -> some View {\n        if read {\n            Image(icon: .general.success)\n                .resizable()\n                .scaledToFit()\n                .frame(width: dimension, height: dimension)\n                .foregroundStyle(.themedSecondary)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ReasonShortcutView.swift",
    "content": "//\n//  ReasonPickerView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 09/10/2024.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ReasonShortcutView: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.dismiss) var dismiss\n    \n    @Binding var reason: String\n    let rulesTarget: (any ProfileProviding)?\n    \n    init(reason: Binding<String>, rulesTarget: (any ProfileProviding)? = nil) {\n        self._reason = reason\n        self.rulesTarget = rulesTarget\n    }\n    \n    var body: some View {\n        HStack(spacing: 12) {\n            ForEach([\n                LocalizedStringResource(\"Spam\"),\n                LocalizedStringResource(\"Troll\"),\n                LocalizedStringResource(\"Abuse\")\n            ], id: \\.key) { item in\n                Text(item)\n                    .padding(.vertical, 8)\n                    .frame(maxWidth: .infinity)\n                    .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: 10))\n                    .contentShape(.rect)\n                    .onTapGesture {\n                        var item = item\n                        // TODO: Set this to instance/community language?\n                        item.locale = .init(languageCode: .english)\n                        reason = String(localized: item)\n                    }\n            }\n            if let rulesTarget, ![BlockNode](rulesTarget.description ?? \"\").rules().isEmpty {\n                Label(\"\\(rulesTarget.name) rules...\", systemImage: \"book.pages\")\n                    .labelStyle(.iconOnly)\n                    .foregroundStyle(.themedAccent)\n                    .padding(.vertical, 8)\n                    .padding(.horizontal, 12)\n                    .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: 10))\n                    .onTapGesture {\n                        navigation.openSheet(.rulesList(rulesTarget, callback: {\n                            reason = $0\n                        }))\n                    }\n            }\n        }\n        .listRowBackground(Color.clear)\n        .listRowInsets(.init())\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/RefreshPopupView.swift",
    "content": "//\n//  RefreshPopupView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 12/07/2024.\n//\n\nimport Haptics\nimport SwiftUI\n\nstruct RefreshPopupView: View {\n    @Environment(HapticManager.self) var hapticManager\n    \n    let title: LocalizedStringResource\n    @Binding var isPresented: Bool\n    let callback: () -> Void\n    \n    init(_ title: LocalizedStringResource, isPresented: Binding<Bool>, callback: @escaping () -> Void) {\n        self.title = title\n        self._isPresented = isPresented\n        self.callback = callback\n    }\n    \n    var body: some View {\n        Group {\n            if isPresented {\n                Button {\n                    isPresented = false\n                    hapticManager.play(haptic: .lightSuccess, tier: .high)\n                    Task { @MainActor in\n                        callback()\n                    }\n                } label: {\n                    HStack(spacing: 0) {\n                        Text(title)\n                            .padding(.horizontal, 10)\n                        Label(\"Refresh\", icon: .general.refresh)\n                            .foregroundStyle(.themedContrastingLabel)\n                            .fontWeight(.semibold)\n                            .padding(.vertical, 4)\n                            .padding(.horizontal, 10)\n                            .background(.themedAccent, in: .capsule)\n                    }\n                }\n                .buttonStyle(.empty)\n                .padding(4)\n                .background(.themedSecondaryBackground, in: .capsule)\n                .shadow(color: .black.opacity(0.1), radius: 5)\n                .shadow(color: .black.opacity(0.1), radius: 1)\n                .padding()\n                .transition(.move(edge: .bottom).combined(with: .opacity))\n            }\n        }\n        .animation(.bouncy, value: isPresented)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/RegistrationApplicationView.swift",
    "content": "//\n//  RegistrationApplicationView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-13.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct RegistrationApplicationView: View {\n    @Environment(\\.palette) var palette\n    \n    let application: RegistrationApplication\n    \n    var body: some View {\n        VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n            HStack {\n                FullyQualifiedLinkView(application.creator, labelStyle: .medium)\n                Spacer()\n                EllipsisMenu(size: 24) {\n                    application.menuActions()\n                }\n            }\n            Markdown(application.questionResponse, configuration: .default(palette: palette))\n            switch application.resolution {\n            case .unresolved:\n                resolutionButtonsView\n            case .approved, .denied:\n                resolutionInfoView\n            }\n        }\n        .padding(Constants.main.standardSpacing)\n        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n        .contextMenu {\n            application.menuActions()\n        }\n    }\n    \n    @ViewBuilder\n    var resolutionInfoView: some View {\n        if let resolver = application.resolver {\n            let color: ThemedColor = application.resolution == .approved ? .themedPositive : .themedNegative\n            let resolverLabel = resolver.nameTextView(\n                showFlairs: false,\n                showInstance: true,\n                font: .footnote,\n                palette: palette,\n                nameColor: color,\n                instanceColor: color.opacity(0.5)\n            )\n            Group {\n                if case let .denied(reason) = application.resolution {\n                    if let reason {\n                        Label(\"Denied by \\(resolverLabel): \\\"\\(reason)\\\"\", icon: .general.failure)\n                    } else {\n                        Label(\"Denied by \\(resolverLabel)\", icon: .general.failure)\n                            .lineLimit(1)\n                    }\n                } else {\n                    Label(\"Approved by \\(resolverLabel)\", icon: .general.success)\n                        .lineLimit(1)\n                }\n            }\n            .symbolVariant(.circle.fill)\n            .foregroundStyle(color)\n            .font(.footnote)\n        }\n    }\n    \n    @ViewBuilder\n    var resolutionButtonsView: some View {\n        HStack(spacing: Constants.main.standardSpacing) {\n            Button {\n                application.showDenialSheet()\n            } label: {\n                Image(icon: .general.failure)\n                    .frame(maxWidth: .infinity)\n                    .padding(.vertical, Constants.main.standardSpacing)\n            }\n            .background(.themedTertiaryGroupedBackground)\n            .foregroundStyle(.themedNegative)\n            .clipShape(.capsule)\n            Button {\n                application.approve()\n            } label: {\n                Image(icon: .general.success)\n                    .frame(maxWidth: .infinity)\n                    .padding(.vertical, Constants.main.standardSpacing)\n            }\n            .background(.themedTertiaryGroupedBackground)\n            .foregroundStyle(.themedAccent)\n            .clipShape(.capsule)\n        }\n        .font(.subheadline)\n        .fontWeight(.semibold)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ReplyView.swift",
    "content": "//\n//  ReplyView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 04/07/2024.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ReplyView: View {\n    @Setting(\\.interactionBar_reply) var replyInteractionBar\n    \n    @Environment(AppState.self) private var appState\n    @Environment(NavigationLayer.self) private var navigation\n    \n    let notification: InboxNotification\n    let comment: Comment\n    \n    var body: some View {\n        VStack(spacing: 0) {\n            VStack(alignment: .leading, spacing: Constants.main.standardSpacing) {\n                HStack {\n                    ExpectedView(comment.creator) { creator in\n                        FullyQualifiedLinkView(creator, labelStyle: .small)\n                    } placeholder: {\n                        Text(verbatim: .personPlaceholder).redacted(reason: .placeholder)\n                    }\n                    Spacer()\n                    Image(icon: (notification.content.type == .mention) ? .lemmy.mention : .lemmy.reply)\n                        .symbolVariant(notification.read ? .none : .fill)\n                        .foregroundStyle(.themedAccent)\n                    EllipsisMenu(size: 24, notification: notification)\n                        .frame(height: 10)\n                }\n                \n                ExpectedView(comment.post) { post in\n                    FooterLinkView(title: post.title, subtitle: nil)\n                }\n                \n                MarkdownWithLinkList(comment.content)\n            }\n            .padding([.top, .horizontal], Constants.main.standardSpacing)\n            \n            InteractionBarView(\n                appState: appState,\n                navigation: navigation,\n                comment: comment,\n                notification: notification,\n                configuration: replyInteractionBar\n            )\n        }\n        .clipped()\n        .background(.themedSecondaryGroupedBackground)\n        .contentShape(.rect)\n        .onTapGesture {\n            navigation.push(.comment(comment))\n        }\n        .quickSwipes(notification: notification, configuration: replyInteractionBar)\n        .clipShape(.rect(cornerRadius: Constants.main.standardSpacing))\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n        .contextMenu(notification: notification)\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ReportView.swift",
    "content": "//\n//  ReportView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-16.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ReportView: View {\n    @Environment(\\.palette) var palette\n    \n    let report: Report\n    \n    var body: some View {\n        targetView\n            .buttonStyle(.empty)\n            .environment(\\.reportContext, report)\n    }\n    \n    @ViewBuilder\n    var targetView: some View {\n        switch report.target {\n        case let .post(post):\n            NavigationLink(.post(post)) {\n                FeedPostView(post: post, overridePostSize: .headline, favoredLink: .creator) {\n                    reportDetailsView\n                    resolutionInfoView\n                }\n            }\n        case let .comment(comment):\n            NavigationLink(.comment(comment)) {\n                FeedCommentView(comment: comment, overriddenSize: .large) {\n                    reportDetailsView\n                    resolutionInfoView\n                }\n            }\n        case let .message(message):\n            MessageView(message: message, notification: nil) {\n                reportDetailsView\n                resolveButton\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var reportDetailsView: some View {\n        VStack(alignment: .leading) {\n            let reporterLabel = report.creator.nameTextView(\n                showFlairs: false,\n                showInstance: true,\n                font: .footnote,\n                palette: palette,\n                nameColor: .themedWarning.opacity(0.5),\n                instanceColor: .themedWarning.opacity(0.3)\n            )\n            Text(\"Reported \\(report.created.getRelativeTime()) by \\(reporterLabel)\")\n                .foregroundStyle(.secondary) // No palette!\n                .font(.footnote)\n                .lineLimit(1)\n            Text(report.reason)\n        }\n        .foregroundStyle(.themedWarning)\n        .frame(maxWidth: .infinity, alignment: .leading)\n        .padding(Constants.main.standardSpacing)\n        .background(\n            .themedWarning.opacity(0.1),\n            in: .rect(cornerRadius: Constants.main.standardSpacing)\n        )\n    }\n    \n    @ViewBuilder\n    var resolutionInfoView: some View {\n        if report.resolved, let resolver = report.resolver {\n            let resolverLabel = resolver.nameTextView(\n                showFlairs: false,\n                showInstance: true,\n                font: .footnote,\n                palette: palette,\n                nameColor: .themedPositive,\n                instanceColor: .themedPositive.opacity(0.5)\n            )\n            Label(\"Resolved by \\(resolverLabel)\", icon: .general.success)\n                .foregroundStyle(.themedPositive)\n                .symbolVariant(.circle.fill)\n                .font(.footnote)\n                .padding(.horizontal, Constants.main.halfSpacing)\n                .lineLimit(1)\n        }\n    }\n    \n    @ViewBuilder\n    var resolveButton: some View {\n        HStack {\n            Button(\n                report.resolved ? \"Resolved\" : \"Resolve\",\n                systemImage: Icons.success\n            ) {\n                report.toggleResolved(feedback: [.haptic])\n            }\n            .foregroundStyle(report.resolved ? .themedContrastingLabel : .themedPrimary)\n            .padding(.vertical, 3)\n            .padding(.horizontal, 8)\n            .imageScale(.small)\n            .background(\n                report.resolved ? .themedPositive : .themedTertiaryGroupedBackground,\n                in: .rect(cornerRadius: Constants.main.standardSpacing)\n            )\n            if report.resolved, let resolver = report.resolver {\n                Text(\"by \\(resolver.fullName)\")\n                    .foregroundStyle(.themedPositive)\n            }\n        }\n        .font(.footnote)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/RulesListView.swift",
    "content": "//\n//  RulesListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-11-11.\n//\n\nimport LemmyMarkdownUI\nimport MlemMiddleware\nimport SwiftUI\n\nstruct RulesListView: View {\n    @Environment(\\.palette) var palette\n    \n    let model: any ProfileProviding\n    @Binding var reason: String\n\n    var body: some View {\n        let rules = [BlockNode](model.description ?? \"\").rules()\n        if !rules.isEmpty {\n            Section {\n                ForEach(Array(rules.enumerated()), id: \\.offset) { index, blocks in\n                    HStack(spacing: 12) {\n                        Image(systemName: \"\\(index + 1).circle.fill\")\n                            .foregroundStyle(.themedSecondary)\n                            .fontWeight(.semibold)\n                        Markdown(blocks, configuration: .default(palette: palette))\n                            .frame(maxWidth: .infinity)\n                    }\n                    .contentShape(.rect)\n                    .onTapGesture {\n                        switch blocks.first {\n                        case let .paragraph(inlines: inlines), .heading(level: _, inlines: let inlines):\n                            let text = inlines.stringLiteral\n                            if text.count < 100 {\n                                reason = \"\\(model.name) rule #\\(index + 1): \\\"\\(text)\\\"\"\n                                return\n                            }\n                        default:\n                            break\n                        }\n                        reason = \"\\(model.name) rule #\\(index + 1)\"\n                    }\n                }\n            } header: {\n                HStack {\n                    CircleCroppedImageView(model, frame: 22)\n                    Text(\"\\(model.name) rules:\")\n                        .foregroundStyle(.themedSecondary)\n                        .textCase(nil)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/RulesPickerView.swift",
    "content": "//\n//  RulesPickerView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-11-11.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct RulesPickerView: View {\n    @Environment(\\.dismiss) var dismiss\n    \n    let model: any ProfileProviding\n    let callback: (String) -> Void\n    \n    var body: some View {\n        Form {\n            RulesListView(model: model, reason: .init(get: { \"\" }, set: {\n                callback($0)\n                dismiss()\n            }))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Home/EventRowView.swift",
    "content": "//\n//  EventRowView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-23.\n//\n\nimport FediverseEvents\nimport SwiftUI\n\nstruct EventRowView: View {\n    @Environment(\\.openURL) var openURL\n\n    let event: Event\n\n    var body: some View {\n        Button {\n            if let url = event.navigationUrl {\n                openURL(url)\n            } \n        } label: {\n            HStack(spacing: 15) {\n                CircleCroppedImageView(\n                    url: event.logos.first?.url,\n                    frame: SearchHomeLabelStyle.iconSize,\n                    fallback: .event\n                )\n                Text(event.name)\n                Spacer()\n                dateView\n                .padding(.trailing, 15)\n            }\n        }\n        .buttonStyle(.chevron)\n    }\n\n    @ViewBuilder\n    var dateView: some View {\n        Group {\n            if event.start < .now {\n                Text(\"Ends \\(event.end, format: .relative(presentation: .numeric, unitsStyle: .wide))\")\n            } else {\n                Text(\"Starts \\(event.start, format: .relative(presentation: .numeric, unitsStyle: .wide))\")\n            }\n        }\n        .font(.footnote)\n        .foregroundStyle(.secondary)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Home/SearchHomeCategoryLabelStyle.swift",
    "content": "//\n//  SearchHomeCategoryLabelStyle.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-24.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct SearchHomeCategoryLabelStyle: LabelStyle {\n    @Environment(\\.palette) var palette\n    @Environment(\\.tint) var tint\n\n    static let iconSize: CGFloat = 80\n\n    private static var innerIconSize: CGFloat {\n        iconSize - 40\n    }\n\n    func makeBody(configuration: Configuration) -> some View {\n            VStack {\n                configuration.icon\n                    .font(.system(size: Self.innerIconSize))\n                    .frame(width: Self.innerIconSize, height: Self.innerIconSize)\n                    .foregroundStyle(.white)\n                    .symbolVariant(.fill)\n                    .padding(20)\n                    .background(tint.gradient(palette: palette), in: .circle)\n                configuration.title\n                    .fontWeight(.semibold)\n                    .font(.subheadline)\n            }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Home/SearchHomeLabelStyle.swift",
    "content": "//\n//  SearchHomeLabelStyle.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-23.\n//\n\nimport SwiftUI\nimport Theming\n\nstruct SearchHomeLabelStyle: LabelStyle {\n    @Environment(\\.palette) var palette\n    @Environment(\\.tint) var tint\n\n    static let iconSize: CGFloat = 35\n\n    func makeBody(configuration: Configuration) -> some View {\n        HStack(spacing: 15) {\n            configuration.icon\n                .symbolVariant(.fill.circle)\n                .foregroundStyle(.white, tint.gradient(palette: palette))\n                .scaledToFit()\n                .font(.system(size: Self.iconSize))\n                .frame(width: Self.iconSize, height: Self.iconSize)\n            configuration.title\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Home/SearchHomeListView.swift",
    "content": "//\n//  SearchHomeListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-23.\n//\n\nimport SwiftUI\n\nstruct SearchHomeListView<Content: View>: View {\n    var content: Content\n\n    init(@ViewBuilder content: () -> Content) {\n        self.content = content()\n    }\n\n    var body: some View {\n        VStack {\n            Group(subviews: content) { subviews in\n                ForEach(Array(subviews.enumerated()), id: \\.element.id) { index, subview in\n                    subview\n                    if index != subviews.count - 1 {\n                        Divider()\n                            .padding(.leading, 50)\n                    }\n                }\n            }\n        }\n        .padding(10)\n        .padding(.trailing, 5)\n        .labelStyle(SearchHomeLabelStyle())\n        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: 25))\n        .paletteBorder(cornerRadius: 25)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Home/SearchHomeView.swift",
    "content": "//\n//  SearchHomeView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-14.\n//\n\nimport ComponentViews\nimport FediverseEvents\nimport Icons\nimport SwiftUI\nimport Theming\n\nstruct SearchHomeView: View {\n    @Environment(\\.navigation) var navigation\n    @Environment(\\.palette) var palette\n\n    @Environment(AppState.self) var appState\n    @Environment(EventsTracker.self) var eventsTracker\n    \n    @Setting(\\.events_showEvents) var showEvents\n\n    var body: some View {\n        VStack(spacing: 20) {\n            if appState.firstAccount.accountType != .guest {\n                subheadingView(\"Visit Again\")\n                topRow\n            }\n            \n            subheadingView(\"Browse\")\n            browseList\n                .padding(.top, 15)\n\n            if let events = eventsTracker.events, !events.isEmpty, showEvents {\n                eventsView(events)\n            }\n        }\n        .padding(.horizontal, 16)\n        .padding(.top, 20)\n    }\n\n    @ViewBuilder\n    func subheadingView(_ text: LocalizedStringResource) -> some View {\n        Text(text)\n            .font(.title)\n            .fontWeight(.semibold)\n            .frame(maxWidth: .infinity, alignment: .leading)\n            .padding(.bottom, -4)\n    }\n    \n    @ViewBuilder\n    var topRow: some View {\n        SearchHomeListView {\n            NavigationLink(\"Saved\", icon: .lemmy.savedFeed, destination: .savedFeed)\n                .tint(.themedSavedFeed)\n            NavigationLink(\"Upvoted\", icon: .lemmy.upvoted, destination: .upvotedFeed)\n                .tint(.themedUpvote)\n        }\n        .buttonStyle(.chevron)\n    }\n\n    @ViewBuilder\n    func eventsView(_ events: [Event]) -> some View {\n        HStack {\n            subheadingView(\"Events\")\n            Spacer()\n            eventsMenuView\n        }\n        .padding(.top, 15)\n        SearchHomeListView {\n            ForEach(events) { \n                EventRowView(event: $0)\n            }\n        }\n    }\n\n    @ViewBuilder\n    var eventsMenuView: some View {\n        Menu(\"More\", icon: .general.menu) {\n            Button(\"Turn Off Events\", icon: .general.hide, role: .destructive) {\n                showEvents = false\n            }\n        }\n        .foregroundStyle(.secondary)\n        .font(.title)\n        .labelStyle(.iconOnly)\n        .padding(.trailing, 10)\n        .padding(.bottom, -6)\n    }\n    \n    @ViewBuilder\n    var browseList: some View {\n        HStack(alignment: .center, spacing: UIDevice.isPad ? 30 : 0) {\n            NavigationLink(\"Communities\", icon: .lemmy.community, destination: .topCommunities)\n                .tint(.themedCommunityAccent)\n\n            if !UIDevice.isPad { Spacer() }\n\n            NavigationLink(\"Users\", icon: .lemmy.person, destination: .topPeople)\n                .tint(.themedPersonAccent)\n\n            if !UIDevice.isPad { Spacer() }\n\n            NavigationLink(\"Instances\", icon: .lemmy.instance, destination: .topInstances)\n                .tint(.themedColorfulAccent(1))\n        }\n        .labelStyle(SearchHomeCategoryLabelStyle())\n        .buttonStyle(.empty)\n        .padding(.horizontal, 20)\n    }\n    \n    @ViewBuilder\n    var browseGrid: some View {\n        LazyVGrid(columns: [.init(.flexible()), .init(.flexible())], spacing: 16) {\n            GridButton(title: \"Top Communities\", color: .themedCommunityAccent)\n            GridButton(title: \"Trending Communities\", color: .themedColorfulAccent(0))\n            GridButton(title: \"Users\", color: .themedPersonAccent)\n            GridButton(title: \"Instances\", color: .themedColorfulAccent(1))\n        }\n        .frame(maxWidth: .infinity)\n        .padding(.horizontal, -4)\n    }\n}\n\nprivate struct GridButton: View {\n    @Environment(\\.palette) var palette\n    \n    let title: LocalizedStringResource\n    let color: ThemedColor\n    \n    var body: some View {\n        ZStack {\n            Text(title)\n                .foregroundStyle(.themedContrastingLabel)\n                .fontWeight(.bold)\n                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)\n                .padding(.horizontal, 15)\n                .padding(.vertical, 10)\n        }\n        .aspectRatio(5 / 3, contentMode: .fit)\n        .frame(maxWidth: .infinity)\n        .background(color.resolve(with: palette).gradient)\n        .clipShape(.rect(cornerRadius: 16))\n        .padding(.horizontal, 4)\n        .onTapGesture {}\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Home/TopCommunitiesListView.swift",
    "content": "//\n//  TopCommunitiesListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-15.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct TopCommunitiesListView: View {\n    @Environment(AppState.self) var appState\n    \n    @State var communityLoader: CommunityFeedLoader?\n\n    var body: some View {\n        FancyScrollView {\n            LazyVStack(spacing: 0) {\n                if let communityLoader {\n                    SearchResultsView(results: communityLoader.items) { community in\n                        CommunityListRow(\n                            community,\n                            readout: .subscribers,\n                            visitContext: .other\n                        )\n                        .onAppear {\n                            do {\n                                try communityLoader.loadIfThreshold(community)\n                            } catch {\n                                handleError(error)\n                            }\n                        }\n                    }\n                    EndOfFeedView(feedLoader: communityLoader, viewType: .hobbit)\n                }\n            }\n            .animation(.easeOut(duration: 0.1), value: communityLoader?.items.isEmpty)\n            .task {\n                do {\n                    if communityLoader == nil {\n                        communityLoader = .init(api: appState.firstApi)\n                        try await communityLoader?.refresh(listing: .all)\n                    }\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n        .background(.themedGroupedBackground)\n        .navigationTitle(\"Communities\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Home/TopInstancesListView.swift",
    "content": "//\n//  TopInstancesListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-15.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct TopInstancesListView: View {\n    @Environment(AppState.self) var appState\n    \n    var body: some View {\n        FancyScrollView {\n            if let errorDetails = MlemStats.main.errorDetails {\n                ErrorView(errorDetails)\n                    .frame(maxWidth: .infinity)\n                    .padding(.top, 40)\n            } else {\n                content\n            }\n        }\n        .background(.themedGroupedBackground)\n        .navigationTitle(\"Instances\")\n    }\n\n    var content: some View {\n        LazyVStack(spacing: 0) {\n            SearchResultsView(results: MlemStats.main.instances ?? []) { instance in\n                InstanceListRow(\n                    instance,\n                    readout: .users,\n                    visitContext: .other\n                )\n            }\n            EndOfFeedView(loadingState: .done, viewType: .hobbit)\n        }\n        .animation(.easeOut(duration: 0.1), value: MlemStats.main.instances?.isEmpty)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Home/TopPeopleListView.swift",
    "content": "//\n//  TopPeopleListView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-15.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct TopPeopleListView: View {\n    @Environment(AppState.self) var appState\n    \n    @State var personLoader: PersonFeedLoader?\n\n    var body: some View {\n        FancyScrollView {\n            LazyVStack(spacing: 0) {\n                if let personLoader {\n                    SearchResultsView(results: personLoader.items) { person in\n                        PersonListRow(\n                            person,\n                            readout: .postsAndComments,\n                            visitContext: .other\n                        )\n                        .onAppear {\n                            do {\n                                try personLoader.loadIfThreshold(person)\n                            } catch {\n                                handleError(error)\n                            }\n                        }\n                    }\n                    EndOfFeedView(feedLoader: personLoader, viewType: .hobbit)\n                }\n            }\n            .animation(.easeOut(duration: 0.1), value: personLoader?.items.isEmpty)\n            .task {\n                do {\n                    if personLoader == nil {\n                        personLoader = .init(api: appState.firstApi)\n                        try await personLoader?.refresh(listing: .all)\n                    }\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n        .background(.themedGroupedBackground)\n        .navigationTitle(\"Users\")\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/PasteLinkButtonView.swift",
    "content": "//\n//  PasteLinkButtonView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 20/06/2024.\n//\n\nimport Dependencies\nimport SwiftUI\n\nstruct PasteLinkButtonView: View {\n    @Environment(\\.openURL) private var openURL\n    \n    var body: some View {\n        Button(\"Open URL from Clipboard\", icon: .general.paste) {\n            if let url = UIPasteboard.general.url {\n                openURL(url)\n            } else if let string = UIPasteboard.general.string,\n                      let url = urlFromString(string),\n                      UIApplication.shared.canOpenURL(url) {\n                openURL(url)\n            } else {\n                ToastModel.main.add(.failure(\"Couldn't read URL\"))\n            }\n        }\n    }\n    \n    func urlFromString(_ string: String) -> URL? {\n        if let url = URL(string: string), UIApplication.shared.canOpenURL(url) {\n            return url\n        }\n        return webfingersToUrl(string)\n    }\n    \n    func webfingersToUrl(_ webfingers: String) -> URL? {\n        do {\n            guard let match = try /[!|@](?<name>[\\w_-]+)@(?<host>[\\w_-]+\\.[\\w_\\-\\.]+)+/.wholeMatch(in: webfingers) else { return nil }\n            \n            let name = match.output.name\n            let host = match.output.host\n            let prefix = webfingers.starts(with: \"@\") ? \"u\" : \"c\"\n            \n            return URL(string: \"https://\\(host)/\\(prefix)/\\(name)\")\n        } catch {\n            return nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Results/CommunityListRow.swift",
    "content": "//\n//  CommunityListRow.swift\n//  Mlem\n//\n//  Created by Sjmarf on 06/07/2024.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct CommunityListRow<Content2: View>: View {\n    typealias Content = CommunityListRowBody<Content2>\n    \n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Setting(\\.interactionBar_community) var communityActionConfiguration\n    \n    let community: Community\n    let content: Content\n    let visitContext: VisitHistory.VisitContext\n\n    init(\n        _ community: Community,\n        complications: [Content.Complication] = [.instance],\n        showBlockStatus: Bool = true,\n        visitContext: VisitHistory.VisitContext = .other,\n        @ViewBuilder content: @escaping () -> Content2\n    ) {\n        self.community = community\n        self.content = .init(community, complications: complications, showBlockStatus: showBlockStatus, content: content)\n        self.visitContext = visitContext\n    }\n    \n    init(\n        _ community: Community,\n        complications: [Content.Complication] = [.instance],\n        showBlockStatus: Bool = true,\n        readout: Content.Readout? = nil,\n        visitContext: VisitHistory.VisitContext = .other\n    ) where Content2 == EmptyView {\n        self.community = community\n        self.content = .init(community, complications: complications, showBlockStatus: showBlockStatus, readout: readout)\n        self.visitContext = visitContext\n    }\n    \n    var body: some View {\n        Button {\n            navigation.push(.community(community, visitContext: visitContext))\n        } label: {\n            FormChevron { content }\n                .padding(.trailing)\n        }\n        .buttonStyle(.empty)\n        .padding(.vertical, 6)\n        .background(.themedSecondaryGroupedBackground)\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n        .contextMenu(community: community)\n        .quickSwipes(community: community, configuration: communityActionConfiguration)\n        .popupAnchor()\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment) {\n//        ScrollView {\n//            ForEach(CommunityMockType.Realistic.allCases) { type in\n//                CommunityListRow(\n//                    Community2.mock(.realistic(type)),\n//                    complications: [.instance],\n//                    readout: .subscribers\n//                )\n//            }\n//        }\n//        .contentMargins(.horizontal, Constants.main.standardSpacing)\n//        .background(.themedGroupedBackground)\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Results/CommunityListRowBody.swift",
    "content": "//\n//  CommunityListRowBody.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-07.\n//\n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct CommunityListRowBody<Content: View>: View {\n    enum Complication { case instance, subscriberCount }\n    enum Readout { case subscribers }\n    \n    @Environment(\\.isEnabled) var isEnabled\n    \n    @Setting(\\.safety_blurNsfw) var blurNsfw\n    \n    let community: Community\n    let showBlockStatus: Bool\n    let complications: [Complication]\n    let readout: Readout?\n    \n    @ViewBuilder let content: () -> Content\n\n    init(\n        _ community: Community,\n        complications: [Complication] = [.instance],\n        showBlockStatus: Bool = true,\n        @ViewBuilder content: @escaping () -> Content\n    ) {\n        self.community = community\n        self.showBlockStatus = showBlockStatus\n        self.readout = nil\n        self.content = content\n        self.complications = complications\n    }\n    \n    init(\n        _ community: Community,\n        complications: [Complication] = [.instance],\n        showBlockStatus: Bool = true,\n        readout: Readout? = nil\n    ) where Content == EmptyView {\n        self.community = community\n        self.showBlockStatus = showBlockStatus\n        self.readout = readout\n        self.content = { EmptyView() }\n        self.complications = complications\n    }\n    \n    var title: String {\n        var title = community.name\n        if community.blocked_.realizedValue, showBlockStatus {\n            title = title + \" ∙ \" + String(localized: \"Blocked\")\n        }\n        if community.nsfw {\n            title = title + \" ∙ \" + String(localized: \"NSFW\")\n        }\n        return title\n    }\n\n    var body: some View {\n        HStack(spacing: Constants.main.standardSpacing) {\n            if community.blocked_.realizedValue, showBlockStatus {\n                Image(icon: .general.hide)\n                    .resizable()\n                    .aspectRatio(contentMode: .fit)\n                    .frame(width: 30, height: 30)\n                    .padding(9)\n            } else {\n                CircleCroppedImageView(\n                    url: community.avatar?.withIconSize(128),\n                    frame: Constants.main.listRowAvatarSize,\n                    fallback: .communityAvatar,\n                    blurred: community.nsfw && (blurNsfw != .never)\n                )\n            }\n            \n            VStack(alignment: .leading, spacing: 2) {\n                Text(title)\n                    .lineLimit(1)\n                    .foregroundStyle(titleColor)\n                caption\n                    .font(.footnote)\n                    .foregroundStyle(.themedSecondary)\n                    .lineLimit(1)\n            }\n            Spacer()\n            switch readout {\n            case .subscribers:\n                subscriberCountReadout\n            case nil:\n                content()\n            }\n        }\n        .padding(.horizontal)\n    }\n    \n    @ViewBuilder\n    var caption: some View {\n        HStack(spacing: 2) {\n            ForEach(Array(complications.enumerated()), id: \\.element) { index, complication in\n                if index != 0 {\n                    Text(verbatim: \"∙\")\n                }\n                Group {\n                    switch complication {\n                    case .instance:\n                        Text(verbatim: \"@\\(community.host)\")\n                    case .subscriberCount:\n                        ExpectedView(community.subscription) { subscription in\n                            HStack(spacing: 2) {\n                                Image(icon: .lemmy.person)\n                                Text(subscription.total.abbreviated)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n    \n    var titleColor: ThemedColor {\n        if community.nsfw {\n            .themedWarning\n        } else {\n            isEnabled ? .themedPrimary : .themedSecondary\n        }\n    }\n    \n    var subscriberCountReadout: some View {\n        let icon: Icon\n        let color: ThemedColor\n        switch community.subscriptionTier {\n        case .favorited:\n            color = .themedFavorite\n            icon = .lemmy.favorite\n        case .subscribed:\n            color = .themedPositive\n            icon = .lemmy.subscribed\n        case .unsubscribed:\n            color = .themedSecondary\n            icon = .lemmy.person\n        }\n        return HStack {\n            Text((community.subscription.value?.total ?? 0).abbreviated)\n            Image(icon: icon)\n                .fontWeight(.semibold)\n        }\n        .monospacedDigit()\n        .foregroundStyle(color)\n        .symbolVariant(.fill)\n        .symbolRenderingMode(.hierarchical)\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment, .sizeThatFitsLayout) {\n//        CommunityListRowBody(\n//            Community2.mock(.generic),\n//            complications: [.instance],\n//            readout: .subscribers\n//        )\n//        .padding(.vertical, Constants.main.standardSpacing)\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Results/InstanceListRow.swift",
    "content": "//\n//  InstanceListRow.swift\n//  Mlem\n//\n//  Created by Sjmarf on 06/07/2024.\n//\n\nimport ComponentViews\nimport MlemBackend\nimport MlemMiddleware\nimport SwiftUI\n\nstruct InstanceListRow<Content2: View>: View {\n    typealias Content = InstanceListRowBody<Content2>\n    \n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    let instance: any InstanceActionProviding\n    let content: Content\n    let visitContext: VisitHistory.VisitContext\n\n    init(\n        _ instance: Instance,\n        @ViewBuilder content: @escaping () -> Content2 = { EmptyView() },\n        showBlockStatus: Bool = true,\n        readout: Content.Readout? = nil,\n        visitContext: VisitHistory.VisitContext = .other\n    ) {\n        self.instance = instance\n        self.content = .init(instance, content: content, showBlockStatus: showBlockStatus, readout: readout)\n        self.visitContext = visitContext\n    }\n    \n    init(\n        _ summary: InstanceSummary,\n        @ViewBuilder content: @escaping () -> Content2 = { EmptyView() },\n        showBlockStatus: Bool = true,\n        readout: Content.Readout? = nil,\n        visitContext: VisitHistory.VisitContext = .other\n    ) where Content2 == EmptyView {\n        self.instance = summary\n        self.content = .init(summary, content: content, showBlockStatus: showBlockStatus, readout: readout)\n        self.visitContext = visitContext\n    }\n    \n    var body: some View {\n        Button {\n            if let instance = instance as? Instance {\n                navigation.push(.instance(instance, visitContext: visitContext))\n            } else {\n                navigation.push(.instanceStub(instance.instanceStub, visitContext: visitContext))\n            }\n        } label: {\n            FormChevron { content }\n                .padding(.trailing)\n        }\n        .buttonStyle(.empty)\n        .padding(.vertical, 6)\n        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n        .contextMenu(instance: instance)\n        .popupAnchor()\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Results/InstanceListRowBody.swift",
    "content": "//\n//  InstanceListRowBody.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-07.\n//\n\nimport Icons\nimport MlemBackend\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nstruct InstanceListRowBody<Content: View>: View {\n    enum Readout { case users }\n\n    @Setting(\\.safety_blurNsfw) var blurNsfw\n    \n    @Environment(\\.isEnabled) var isEnabled\n    \n    let instance: Instance?\n    let summary: InstanceSummary?\n    let readout: Readout?\n    let showBlockStatus: Bool\n    \n    @ViewBuilder let content: () -> Content\n\n    init(\n        _ instance: Instance,\n        @ViewBuilder content: @escaping () -> Content = { EmptyView() },\n        showBlockStatus: Bool = true,\n        readout: Readout? = nil\n    ) {\n        self.instance = instance\n        self.summary = nil\n        self.showBlockStatus = showBlockStatus\n        self.content = content\n        self.readout = readout\n    }\n    \n    init(\n        _ summary: InstanceSummary,\n        @ViewBuilder content: @escaping () -> Content = { EmptyView() },\n        showBlockStatus: Bool = true,\n        readout: Readout? = nil\n    ) {\n        self.summary = summary\n        self.instance = nil\n        self.showBlockStatus = showBlockStatus\n        self.content = content\n        self.readout = readout\n    }\n    \n    var isBlocked: Bool {\n        guard showBlockStatus else { return false }\n        if let instance {\n            return instance.blocked_.realizedValue\n        }\n        if let summary, let session = AppState.main.firstSession as? UserSession, let blocks = session.blocks {\n            let actorId = ActorIdentifier.instance(host: summary.host)\n            return blocks.contains(instanceActorId: actorId)\n        }\n        return false\n    }\n    \n    var title: String {\n        let hostText = instance?.host ?? summary?.host ?? \"\"\n        if isBlocked {\n            return hostText + \" ∙ \" + String(localized: \"Blocked\")\n        }\n        return hostText\n    }\n    \n    var avatar: URL? {\n        instance?.avatar ?? summary?.avatar\n    }\n    \n    var software: SiteSoftware? {\n        instance?.software.value ?? summary.map { .init(from: $0.software) }\n    }\n\n    var body: some View {\n        HStack(spacing: Constants.main.standardSpacing) {\n            if isBlocked {\n                Image(icon: .general.hide)\n                    .resizable()\n                    .aspectRatio(contentMode: .fit)\n                    .frame(width: 30, height: 30)\n                    .padding(9)\n            } else {\n                CircleCroppedImageView(\n                    url: avatar?.withIconSize(128),\n                    frame: Constants.main.listRowAvatarSize,\n                    fallback: .instanceAvatar\n                )\n            }\n            \n            VStack(alignment: .leading, spacing: 2) {\n                Text(title)\n                    .foregroundStyle(isEnabled ? .themedPrimary : .themedSecondary)\n                    .lineLimit(1)\n                if let software {\n                    Text(software.label)\n                        .font(.footnote)\n                        .foregroundStyle(.themedSecondary)\n                        .lineLimit(1)\n                }\n            }\n            Spacer()\n            switch readout {\n            case .users:\n                userCountReadout\n            case nil:\n                content()\n            }\n        }\n        .padding(.horizontal)\n        .contentShape(.rect)\n    }\n    \n    var userCountReadout: some View {\n        HStack {\n            Text((instance?.userCount.value ?? summary?.totalUsers ?? 0).abbreviated)\n            Image(icon: .lemmy.person)\n                .symbolVariant(.fill)\n                .fontWeight(.semibold)\n        }\n        .monospacedDigit()\n        .foregroundStyle(.themedSecondary)\n        .symbolRenderingMode(.hierarchical)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Results/PersonListRow.swift",
    "content": "//\n//  PersonListRow.swift\n//  Mlem\n//\n//  Created by Sjmarf on 06/07/2024.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct PersonListRow<Content2: View>: View {\n    typealias Content = PersonListRowBody<Content2>\n    \n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.communityContext) var communityContext\n    \n    let person: Person\n    let content: Content\n    let visitContext: VisitHistory.VisitContext\n\n    init(\n        _ person: Person,\n        complications: [Content.Complication] = [.instance],\n        showBlockStatus: Bool = true,\n        visitContext: VisitHistory.VisitContext = .other,\n        @ViewBuilder content: @escaping () -> Content2\n    ) {\n        self.person = person\n        self.content = .init(person, complications: complications, showBlockStatus: showBlockStatus, content: content)\n        self.visitContext = visitContext\n    }\n    \n    init(\n        _ person: Person,\n        complications: [Content.Complication] = [.instance],\n        showBlockStatus: Bool = true,\n        readout: Content.Readout? = nil,\n        visitContext: VisitHistory.VisitContext = .other\n    ) where Content2 == EmptyView {\n        self.person = person\n        self.content = .init(person, complications: complications, showBlockStatus: showBlockStatus, readout: readout)\n        self.visitContext = visitContext\n    }\n    \n    var body: some View {\n        Button {\n            navigation.push(.person(person, visitContext: visitContext))\n        } label: {\n            FormChevron { content }\n                .padding(.trailing)\n        }\n        .buttonStyle(.empty)\n        .padding(.vertical, 6)\n        .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing))\n        .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing))\n        .contextMenu(person: person)\n        .popupAnchor()\n        .paletteBorder(cornerRadius: Constants.main.standardSpacing)\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment) {\n//        ScrollView {\n//            ForEach(PersonMockType.Realistic.allCases) { type in\n//                PersonListRow(\n//                    Person2.mock(.realistic(type)),\n//                    complications: [.instance, .date],\n//                    readout: .postsAndComments\n//                )\n//            }\n//        }\n//        .contentMargins(.horizontal, Constants.main.standardSpacing)\n//        .background(.themedGroupedBackground)\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Results/PersonListRowBody.swift",
    "content": "//\n//  PersonListRowBody.swift\n//  Mlem\n//\n//  Created by Sjmarf on 28/06/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct PersonListRowBody<Content: View>: View {\n    enum Complication { case instance, date }\n    enum Readout { case postsAndComments }\n    \n    @Environment(\\.communityContext) var communityContext\n    @Environment(\\.isEnabled) var isEnabled\n    \n    let person: Person\n    var showBlockStatus: Bool = true\n    let complications: [Complication]\n    let readout: Readout?\n    \n    @ViewBuilder let content: () -> Content\n\n    init(\n        _ person: Person,\n        complications: [Complication] = [.instance],\n        showBlockStatus: Bool = true,\n        @ViewBuilder content: @escaping () -> Content\n    ) {\n        self.person = person\n        self.showBlockStatus = showBlockStatus\n        self.readout = nil\n        self.content = content\n        self.complications = complications\n    }\n    \n    init(\n        _ person: Person,\n        complications: [Complication] = [.instance],\n        showBlockStatus: Bool = true,\n        readout: Readout? = nil\n    ) where Content == EmptyView {\n        self.person = person\n        self.showBlockStatus = showBlockStatus\n        self.readout = readout\n        self.content = { EmptyView() }\n        self.complications = complications\n    }\n    \n    var title: String {\n        if person.blocked_.realizedValue, showBlockStatus {\n            return person.displayName + \" ∙ \" + String(localized: \"Blocked\")\n        } else {\n            return person.displayName\n        }\n    }\n    \n    var body: some View {\n        HStack(spacing: Constants.main.standardSpacing) {\n            if person.blocked_.realizedValue, showBlockStatus {\n                Image(icon: .general.hide)\n                    .resizable()\n                    .aspectRatio(contentMode: .fit)\n                    .frame(width: 30, height: 30)\n                    .padding(9)\n            } else {\n                CircleCroppedImageView(\n                    url: person.avatar?.withIconSize(128),\n                    frame: Constants.main.listRowAvatarSize,\n                    fallback: .personAvatar\n                )\n            }\n            VStack(alignment: .leading, spacing: 4) {\n                (flairs.textView + Text(title))\n                    .foregroundStyle(isEnabled ? .themedPrimary : .themedSecondary)\n                    .lineLimit(1)\n                    .imageScale(.small)\n                    .symbolVariant(.fill)\n                caption\n                    .font(.footnote)\n                    .foregroundStyle(.themedSecondary)\n                    .lineLimit(1)\n            }\n            Spacer()\n            switch readout {\n            case .postsAndComments:\n                postsAndCommentsReadout\n            case nil:\n                content()\n            }\n        }\n        .padding(.horizontal)\n        .padding(.vertical, -5)\n        .contentShape(.rect)\n        .padding(.vertical, 5)\n    }\n    \n    var dateFormatter: DateFormatter {\n        let dateFormatter = DateFormatter()\n        dateFormatter.dateFormat = \"yyyy\"\n        return dateFormatter\n    }\n    \n    @ViewBuilder\n    var caption: some View {\n        HStack(spacing: 2) {\n            ForEach(Array(complications.enumerated()), id: \\.element) { index, complication in\n                if index != 0 {\n                    Text(verbatim: \"∙\")\n                }\n                Group {\n                    switch complication {\n                    case .instance:\n                        Text(verbatim: \"@\\(person.host)\")\n                    case .date:\n                        Text(dateFormatter.string(from: person.created))\n                    }\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var postsAndCommentsReadout: some View {\n        HStack(spacing: 5) {\n            VStack(alignment: .trailing, spacing: 6) {\n                Text((person.postCount.value ?? 0).abbreviated)\n                Text((person.commentCount.value ?? 0).abbreviated)\n            }\n            .foregroundStyle(.secondary)\n            .font(.subheadline)\n            .monospacedDigit()\n            VStack(spacing: 10) {\n                Image(icon: .lemmy.post)\n                Image(icon: .lemmy.comment)\n            }\n            .imageScale(.small)\n        }\n        .foregroundStyle(.themedSecondary)\n    }\n    \n    var flairs: [PersonFlair] {\n        person.flairs(communityContext: communityContext)\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment, .sizeThatFitsLayout) {\n//        PersonListRowBody(\n//            Person2.mock(.generic),\n//            complications: [.instance, .date],\n//            readout: .postsAndComments\n//        )\n//        .padding(.vertical, Constants.main.standardSpacing)\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchBar/DefaultTextInputType.swift",
    "content": "// This code taken from the open-source SwiftUIX library https://github.com/SwiftUIX/SwiftUIX/blob/cf729fcab44196ed7361293bcad493a0e928fb24/Sources/Intermodular/Helpers/SwiftUI/DefaultTextInputType.swift#L10\n// Copyright (c) Vatsal Manot\n//\n\nimport Combine\nimport Swift\nimport SwiftUI\n\n// MARK: - Extensions\n\npublic extension SearchBar {\n    init(\n        _ title: LocalizedStringResource,\n        text: Binding<String>,\n        isEditing: Binding<Bool>,\n        onCommit: @escaping () -> Void = {}\n    ) {\n        self.init(\n            title,\n            text: text,\n            onEditingChanged: { isEditing.wrappedValue = $0 },\n            onCommit: onCommit\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchBar/SearchBar+NavigationView.swift",
    "content": "//\n// This code taken from the open-source SwiftUIX library. https://github.com/SwiftUIX/SwiftUIX/blob/master/Sources/Intramodular/Search%20Bar/SearchBar%2BNavigationView.swift\n//\n// Copyright (c) Vatsal Manot\n//\n\nimport Swift\nimport SwiftUI\n\n#if (os(iOS) && canImport(CoreTelephony)) || targetEnvironment(macCatalyst)\n\n    @available(macCatalystApplicationExtension, unavailable)\n    @available(iOSApplicationExtension, unavailable)\n    @available(tvOSApplicationExtension, unavailable)\n    private struct _NavigationSearchBarConfigurator<SearchResultsContent: View>: UIViewControllerRepresentable {\n        let searchBar: SearchBar\n        let searchResultsContent: () -> SearchResultsContent\n    \n        @Environment(\\._hidesNavigationSearchBarWhenScrolling) var hidesSearchBarWhenScrolling: Bool?\n    \n        var automaticallyShowSearchBar: Bool? = true\n        var hideNavigationBarDuringPresentation: Bool?\n        var obscuresBackgroundDuringPresentation: Bool?\n    \n        func makeUIViewController(context: Context) -> UIViewControllerType {\n            UIViewControllerType(coordinator: context.coordinator)\n        }\n    \n        func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {\n            context.coordinator.base = self\n            context.coordinator.searchBarCoordinator.base = searchBar\n\n            searchBar._updateUISearchBar(context.coordinator.searchController.searchBar, environment: context.environment)\n        }\n    \n        func makeCoordinator() -> Coordinator {\n            Coordinator(base: self, searchBarCoordinator: .init(base: searchBar))\n        }\n    }\n\n    @available(macCatalystApplicationExtension, unavailable)\n    @available(iOSApplicationExtension, unavailable)\n    @available(tvOSApplicationExtension, unavailable)\n    extension _NavigationSearchBarConfigurator {\n        fileprivate class SearchController: UISearchController {\n            private var customSearchBar: UISearchBar?\n        \n            override var searchBar: UISearchBar {\n                if let customSearchBar {\n                    return customSearchBar\n                } else {\n                    customSearchBar = UISearchBar(frame: .zero)\n                    return customSearchBar!\n                }\n            }\n        \n            override init(\n                searchResultsController: UIViewController?\n            ) {\n                super.init(searchResultsController: searchResultsController)\n            }\n        \n            @available(*, unavailable)\n            required init?(coder: NSCoder) {\n                fatalError(\"init(coder:) has not been implemented\")\n            }\n        }\n    \n        class Coordinator: NSObject, UISearchBarDelegate, UISearchControllerDelegate, UISearchResultsUpdating {\n            fileprivate var base: _NavigationSearchBarConfigurator\n            fileprivate var searchBarCoordinator: SearchBar.Coordinator\n            fileprivate var searchController: SearchController!\n        \n            fileprivate weak var uiViewController: UIViewController? {\n                didSet {\n                    if uiViewController == nil || uiViewController != oldValue {\n                        if oldValue?.searchController != nil {\n                            oldValue?.searchController = nil\n                        }\n                    }\n                \n                    updateSearchController()\n                }\n            }\n        \n            fileprivate init(\n                base: _NavigationSearchBarConfigurator,\n                searchBarCoordinator: SearchBar.Coordinator\n            ) {\n                self.base = base\n                self.searchBarCoordinator = searchBarCoordinator\n            \n                super.init()\n            \n                initializeSearchController()\n                updateSearchController()\n            }\n        \n            private func initializeSearchController() {\n                let searchResultsController: UIViewController?\n                let searchResultsContent = base.searchResultsContent()\n            \n                if searchResultsContent is EmptyView {\n                    searchResultsController = nil\n                } else {\n                    searchResultsController = UIHostingController<SearchResultsContent>(rootView: base.searchResultsContent())\n                }\n            \n                searchController = SearchController(\n                    searchResultsController: searchResultsController\n                )\n                searchController.definesPresentationContext = true\n                searchController.obscuresBackgroundDuringPresentation = false\n                searchController.searchBar.delegate = self\n                searchController.searchResultsUpdater = self\n            }\n        \n            private func updateSearchController() {\n                guard let uiViewController else {\n                    return\n                }\n            \n                if uiViewController.searchController !== searchController {\n                    uiViewController.searchController = searchController\n                }\n            \n                if let obscuresBackgroundDuringPresentation = base.obscuresBackgroundDuringPresentation {\n                    searchController.obscuresBackgroundDuringPresentation = obscuresBackgroundDuringPresentation\n                } else {\n                    searchController.obscuresBackgroundDuringPresentation = false\n                }\n            \n                if let hideNavigationBarDuringPresentation = base.hideNavigationBarDuringPresentation {\n                    searchController.hidesNavigationBarDuringPresentation = hideNavigationBarDuringPresentation\n                }\n            \n                (\n                    searchController.searchResultsController as? UIHostingController<SearchResultsContent>\n                )?.rootView = base.searchResultsContent()\n            \n                if let hidesSearchBarWhenScrolling = base.hidesSearchBarWhenScrolling {\n                    uiViewController.hidesSearchBarWhenScrolling = hidesSearchBarWhenScrolling\n                }\n            \n                if let automaticallyShowSearchBar = base.automaticallyShowSearchBar, automaticallyShowSearchBar {\n                    uiViewController.sizeToFitSearchBar()\n                }\n            }\n        \n            // MARK: - UISearchBarDelegate\n        \n            public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {\n                searchBarCoordinator.searchBarTextDidBeginEditing(searchBar)\n            }\n        \n            public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {\n                searchBarCoordinator.searchBar(searchBar, textDidChange: searchText)\n            }\n        \n            public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {\n                searchBarCoordinator.searchBarTextDidEndEditing(searchBar)\n            }\n        \n            public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {\n                searchController.isActive = false\n\n                searchBarCoordinator.searchBarCancelButtonClicked(searchBar)\n            }\n        \n            public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {\n                searchBarCoordinator.searchBarSearchButtonClicked(searchBar)\n            }\n        \n            // MARK: UISearchControllerDelegate\n        \n            func willPresentSearchController(_ searchController: UISearchController) {}\n        \n            func didPresentSearchController(_ searchController: UISearchController) {}\n        \n            func willDismissSearchController(_ searchController: UISearchController) {}\n        \n            func didDismissSearchController(_ searchController: UISearchController) {}\n        \n            // MARK: UISearchResultsUpdating\n        \n            func updateSearchResults(for searchController: UISearchController) {}\n        }\n    \n        class UIViewControllerType: UIViewController {\n            weak var coordinator: Coordinator?\n        \n            init(coordinator: Coordinator?) {\n                self.coordinator = coordinator\n            \n                super.init(nibName: nil, bundle: nil)\n            }\n        \n            @available(*, unavailable)\n            required init?(coder: NSCoder) {\n                fatalError(\"init(coder:) has not been implemented\")\n            }\n        \n            override func willMove(toParent parent: UIViewController?) {\n                super.willMove(toParent: parent)\n            \n                coordinator?.uiViewController = navigationController?.viewControllers.first\n            }\n        \n            override func viewWillAppear(_ animated: Bool) {\n                super.viewWillAppear(animated)\n            \n                coordinator?.uiViewController = navigationController?.viewControllers.first\n            }\n        }\n    }\n\n    // MARK: - API\n\n    public extension View {\n        /// Sets the navigation search bar for this view.\n        @available(macCatalystApplicationExtension, unavailable)\n        @available(iOSApplicationExtension, unavailable)\n        @available(tvOSApplicationExtension, unavailable)\n        func navigationSearchBar(_ searchBar: () -> SearchBar) -> some View {\n            background(_NavigationSearchBarConfigurator(searchBar: searchBar(), searchResultsContent: { EmptyView() }))\n        }\n    \n        /// Hides the integrated search bar when scrolling any underlying content.\n        func navigationSearchBarHiddenWhenScrolling(_ hidesSearchBarWhenScrolling: Bool) -> some View {\n            environment(\\._hidesNavigationSearchBarWhenScrolling, hidesSearchBarWhenScrolling)\n        }\n    }\n\n    private class DefaultEnvironmentKey<Value>: EnvironmentKey {\n        public static var defaultValue: Value? {\n            nil\n        }\n    }\n\n    // MARK: - Auxiliary\n\n    extension EnvironmentValues {\n        private class _HidesNavigationSearchBarWhenScrolling: DefaultEnvironmentKey<Bool> {}\n    \n        var _hidesNavigationSearchBarWhenScrolling: Bool? {\n            get {\n                self[_HidesNavigationSearchBarWhenScrolling.self]\n            } set {\n                self[_HidesNavigationSearchBarWhenScrolling.self] = newValue\n            }\n        }\n    }\n\n    // MARK: - Helpers\n\n    private extension UIViewController {\n        var searchController: UISearchController? {\n            get {\n                navigationItem.searchController\n            } set {\n                navigationItem.searchController = newValue\n            }\n        }\n    \n        var hidesSearchBarWhenScrolling: Bool {\n            get {\n                navigationItem.hidesSearchBarWhenScrolling\n            } set {\n                navigationItem.hidesSearchBarWhenScrolling = newValue\n            }\n        }\n    \n        func sizeToFitSearchBar() {\n            navigationController?.navigationBar.sizeToFit()\n        }\n    }\n\n#endif\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchBar/SearchBar.swift",
    "content": "//\n// Modified version of code taken from the open-source SwiftUIX library. https://github.com/SwiftUIX/SwiftUIX/blob/master/Sources/Intramodular/Search%20Bar/SearchBar.swift\n//\n// Copyright (c) Vatsal Manot\n//\n\nimport Swift\nimport SwiftUI\n\n#if (os(iOS) && canImport(CoreTelephony)) || os(macOS) || targetEnvironment(macCatalyst)\n\n    /// A specialized view for receiving search-related information from the user.\n    public struct SearchBar {\n        @Binding fileprivate var text: String\n    \n//    var customAppKitOrUIKitClass: AppKitOrUIKitSearchBar.Type? // UISearchBar\n    \n        private let onEditingChanged: (Bool) -> Void\n        private let onCommit: () -> Void\n        private var isInitialFirstResponder: Bool?\n        private var isFocused: Binding<Bool>?\n    \n        private var placeholder: String?\n\n        #if os(iOS) || targetEnvironment(macCatalyst)\n            private var iconImageConfiguration: [UISearchBar.Icon: UIImage] = [:]\n        #endif\n    \n        private var showsCancelButton: Bool?\n        private var onCancel: () -> Void = {}\n    \n        #if os(iOS) || targetEnvironment(macCatalyst)\n            private var returnKeyType: UIReturnKeyType?\n            private var enablesReturnKeyAutomatically: Bool?\n            private var isSecureTextEntry: Bool = false\n            private var textContentType: UITextContentType?\n            private var keyboardType: UIKeyboardType?\n        #endif\n    \n        public init(\n            _ title: LocalizedStringResource,\n            text: Binding<String>,\n            onEditingChanged: @escaping (Bool) -> Void = { _ in },\n            onCommit: @escaping () -> Void = {}\n        ) {\n            self.placeholder = .init(localized: title)\n            self._text = text\n            self.onCommit = onCommit\n            self.onEditingChanged = onEditingChanged\n        }\n\n        public init(\n            text: Binding<String>,\n            onEditingChanged: @escaping (Bool) -> Void = { _ in },\n            onCommit: @escaping () -> Void = {}\n        ) {\n            self._text = text\n            self.onCommit = onCommit\n            self.onEditingChanged = onEditingChanged\n        }\n    }\n\n    @available(macCatalystApplicationExtension, unavailable)\n    @available(iOSApplicationExtension, unavailable)\n    @available(tvOSApplicationExtension, unavailable)\n    extension SearchBar: UIViewRepresentable {\n        public typealias UIViewType = UISearchBar\n    \n        public func makeUIView(context: Context) -> UIViewType {\n            let uiView = _UISearchBar()\n        \n            uiView.delegate = context.coordinator\n\n            if context.environment.isEnabled {\n                DispatchQueue.main.async {\n                    if (isInitialFirstResponder ?? isFocused?.wrappedValue) ?? false {\n                        uiView.becomeFirstResponder()\n                    }\n                }\n            }\n\n            return uiView\n        }\n    \n        public func updateUIView(_ uiView: UIViewType, context: Context) {\n            if #available(iOS 26, *) {\n                uiView.backgroundColor = .clear\n                uiView.barTintColor = .clear\n                uiView.setBackgroundImage(UIImage(), for: .any, barMetrics: .default)\n                uiView.isTranslucent = true\n            }\n            \n            context.coordinator.base = self\n        \n            _updateUISearchBar(uiView, environment: context.environment)\n        }\n    \n        func _updateUISearchBar(\n            _ uiView: UIViewType,\n            environment: EnvironmentValues\n        ) {\n            uiView.isUserInteractionEnabled = environment.isEnabled\n\n            do {\n                uiView.searchTextField.autocorrectionType = environment.disableAutocorrection.map { $0 ? .no : .yes } ?? .default\n            \n                if let placeholder {\n                    uiView.placeholder = placeholder\n                }\n\n                for (icon, image) in iconImageConfiguration where uiView.image(\n                    for: icon, state: .normal\n                ) == nil { // FIXME: This is a performance hack.\n                    uiView.setImage(image, for: icon, state: .normal)\n                }\n\n                if let showsCancelButton {\n                    if uiView.showsCancelButton != showsCancelButton {\n                        uiView.setShowsCancelButton(showsCancelButton, animated: true)\n                    }\n                }\n            }\n        \n            do {\n                _assignIfNotEqual(returnKeyType ?? .default, to: &uiView.returnKeyType)\n                _assignIfNotEqual(keyboardType ?? .default, to: &uiView.keyboardType)\n                _assignIfNotEqual(enablesReturnKeyAutomatically ?? false, to: &uiView.enablesReturnKeyAutomatically)\n            }\n        \n            do {\n                if uiView.text != text {\n                    uiView.text = text\n                }\n            \n                if !uiView.searchTextField.tokens.isEmpty {\n                    uiView.searchTextField.tokens = []\n                }\n            }\n\n            (uiView as? _UISearchBar)?.isFirstResponderBinding = isFocused\n\n            do {\n                // version of below with no responder binding. it's not a pretty hack but it does work\n                // note that switching tabs with search selected will result in search still displaying \"search for communities and users,\"\n                // but since the keyboard hides the tab bar that probably won't come up for 99% of users\n                if let isFocused, environment.isEnabled {\n                    if isFocused.wrappedValue, !uiView.isFirstResponder {\n                        DispatchQueue.main.async {\n                            uiView.becomeFirstResponder()\n                        }\n                    } else if !isFocused.wrappedValue, uiView.isFirstResponder {\n                        DispatchQueue.main.async {\n                            uiView.resignFirstResponder()\n                        }\n                    }\n                }\n                \n//                if let uiView = uiView as? _UISearchBar, environment.isEnabled {\n//                    DispatchQueue.main.async {\n//                        if let isFocused, uiView.window != nil {\n//                            uiView.isFirstResponderBinding = isFocused\n//\n//                            if isFocused.wrappedValue, !uiView.isFirstResponder {\n//                                uiView.becomeFirstResponder()\n//                            } else if !isFocused.wrappedValue, uiView.isFirstResponder {\n//                                uiView.resignFirstResponder()\n//                            }\n//                        }\n//                    }\n//                }\n            }\n        }\n    \n        public class Coordinator: NSObject, UISearchBarDelegate {\n            var base: SearchBar\n        \n            init(base: SearchBar) {\n                self.base = base\n            }\n        \n            public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {\n                base.isFocused?.removeDuplicates().wrappedValue = true\n            \n                base.onEditingChanged(true)\n            }\n        \n            public func searchBar(_ searchBar: UIViewType, textDidChange searchText: String) {\n                base.text = searchText\n            }\n\n            public func searchBarShouldEndEditing(_ searchBar: UISearchBar) -> Bool {\n                true\n            }\n\n            public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {\n                base.isFocused?.removeDuplicates().wrappedValue = false\n            \n                base.onEditingChanged(false)\n            }\n        \n            public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {\n                searchBar.endEditing(true)\n            \n                base.isFocused?.removeDuplicates().wrappedValue = false\n\n                base.onCancel()\n            }\n        \n            public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {\n                searchBar.endEditing(true)\n            \n                // base.isFocused?.removeDuplicates().wrappedValue = false\n\n                base.onCommit()\n            }\n        }\n    \n        public func makeCoordinator() -> Coordinator {\n            Coordinator(base: self)\n        }\n    }\n\n    // MARK: - API\n\n    public extension SearchBar {\n        @available(macCatalystApplicationExtension, unavailable)\n        @available(iOSApplicationExtension, unavailable)\n        @available(tvOSApplicationExtension, unavailable)\n        func isInitialFirstResponder(_ isInitialFirstResponder: Bool) -> Self {\n            then { $0.isInitialFirstResponder = isInitialFirstResponder }\n        }\n\n        @available(macCatalystApplicationExtension, unavailable)\n        @available(iOSApplicationExtension, unavailable)\n        @available(tvOSApplicationExtension, unavailable)\n        func focused(_ isFocused: Binding<Bool>) -> Self {\n            then { $0.isFocused = isFocused }\n        }\n    }\n\n    @available(macCatalystApplicationExtension, unavailable)\n    @available(iOSApplicationExtension, unavailable)\n    @available(tvOSApplicationExtension, unavailable)\n    public extension SearchBar {\n        #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst)\n            func placeholder(_ placeholder: String?) -> Self {\n                then { $0.placeholder = placeholder }\n            }\n        #endif\n    \n        func showsCancelButton(_ showsCancelButton: Bool) -> Self {\n            then { $0.showsCancelButton = showsCancelButton }\n        }\n    \n        func onCancel(perform action: @escaping () -> Void) -> Self {\n            then { $0.onCancel = action }\n        }\n    \n        func returnKeyType(_ returnKeyType: UIReturnKeyType) -> Self {\n            then { $0.returnKeyType = returnKeyType }\n        }\n    \n        func enablesReturnKeyAutomatically(_ enablesReturnKeyAutomatically: Bool) -> Self {\n            then { $0.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically }\n        }\n    \n        func textContentType(_ textContentType: UITextContentType?) -> Self {\n            then { $0.textContentType = textContentType }\n        }\n    \n        func keyboardType(_ keyboardType: UIKeyboardType) -> Self {\n            then { $0.keyboardType = keyboardType }\n        }\n    }\n\n    // MARK: - Auxiliary\n\n    #if os(iOS) || targetEnvironment(macCatalyst)\n        private final class _UISearchBar: UISearchBar {\n            var isFirstResponderBinding: Binding<Bool>?\n        \n            override init(frame: CGRect) {\n                super.init(frame: frame)\n            }\n    \n            @available(*, unavailable)\n            required init?(coder: NSCoder) {\n                fatalError(\"init(coder:) has not been implemented\")\n            }\n    \n            @discardableResult\n            override func becomeFirstResponder() -> Bool {\n                let result = super.becomeFirstResponder()\n        \n                isFirstResponderBinding?.wrappedValue = result\n        \n                return result\n            }\n    \n            @discardableResult\n            override func resignFirstResponder() -> Bool {\n                let result = super.resignFirstResponder()\n        \n                isFirstResponderBinding?.wrappedValue = !result\n        \n                return result\n            }\n        }\n    #endif\n\n    // MARK: - Development Preview -\n\n    #if (os(iOS) && canImport(CoreTelephony)) || targetEnvironment(macCatalyst)\n        @available(macCatalystApplicationExtension, unavailable)\n        @available(iOSApplicationExtension, unavailable)\n        @available(tvOSApplicationExtension, unavailable)\n        struct SearchBar_Previews: PreviewProvider {\n            static var previews: some View {\n                SearchBar(\"Search...\", text: .constant(\"\"))\n            }\n        }\n    #endif\n#endif\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchBar/SearchBarExtensions.swift",
    "content": "//\n// Modified version of code taken from the open-source SwiftUIX library.\n//\n// Copyright (c) Vatsal Manot\n//\n\nimport SwiftUI\n\npublic extension View {\n    @inlinable\n    func then(_ body: (inout Self) -> Void) -> Self {\n        var result = self\n        \n        body(&result)\n        \n        return result\n    }\n}\n\npublic extension Binding {\n    func removeDuplicates() -> Self where Value: Equatable {\n        .init(\n            get: { self.wrappedValue },\n            set: { newValue in\n                let oldValue = self.wrappedValue\n                \n                guard newValue != oldValue else {\n                    return\n                }\n                \n                self.wrappedValue = newValue\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchBar/View+WithSheetSearch.swift",
    "content": "//\n//  View+withSheetSearch.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-08-22.\n//\n\nimport ComponentViews\nimport SwiftUI\n\nprivate struct SearchSheetViewModifier: ViewModifier {\n    @Environment(NavigationLayer.self) var navigation\n    \n    @Binding var query: String\n    @FocusState var focused: Bool\n    \n    func body(content: Content) -> some View {\n        Group {\n            if #available(iOS 26, *) {\n                ios26Body(content: content)\n            } else {\n                ios18Body(content: content)\n            }\n        }\n        .toolbar {\n            CloseButtonToolbarItem(ios18Label: .cancel) {\n                navigation.dismissSheet()\n            }\n        }\n        .onAppear {\n            focused = true\n        }\n    }\n    \n    func ios18Body(content: Content) -> some View {\n        content\n            .toolbar {\n                ToolbarItem(placement: .principal) {\n                    HStack(spacing: 0) {\n                        SearchBar(\"Search\", text: $query, isEditing: .constant(true))\n                            .isInitialFirstResponder(true)\n                            .focused($focused)\n                            .autocorrectionDisabled()\n                    }\n                }\n            }\n    }\n    \n    func ios26Body(content: Content) -> some View {\n        content\n            .toolbar {\n                ToolbarItem(placement: .bottomBar) {\n                    HStack(spacing: 0) {\n                        SearchBar(\"Search\", text: $query, isEditing: .constant(true))\n                            .isInitialFirstResponder(true)\n                            .focused($focused)\n                            .autocorrectionDisabled()\n                    }\n                    .padding(-10)\n                }\n            }\n    }\n}\n\nextension View {\n    func withSheetSearch(query: Binding<String>) -> some View {\n        modifier(SearchSheetViewModifier(query: query))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchBar/_assignIfNotEqual.swift",
    "content": "//\n// This code taken from the open-source SwiftUIX library. https://github.com/SwiftUIX/SwiftUIX/blob/cf729fcab44196ed7361293bcad493a0e928fb24/Sources/Intramodular/Miscellaneous/_assignIfNotEqual.swift#L58\n//\n// Copyright (c) Vatsal Manot\n//\n\nimport Swift\nimport SwiftUI\n\n@_spi(Internal)\n@_transparent\npublic func _assignIfNotEqual<Value: Equatable>(\n    _ value: Value,\n    to destination: inout Value\n) {\n    if value != destination {\n        destination = value\n    }\n}\n\npublic extension NSObjectProtocol {\n    @_spi(Internal)\n    @_transparent\n    func _assignIfNotEqual<Value: Equatable>(\n        _ newValue: Value,\n        to keyPath: ReferenceWritableKeyPath<Self, Value>\n    ) {\n        if self[keyPath: keyPath] != newValue {\n            self[keyPath: keyPath] = newValue\n        }\n    }\n    \n    @_spi(Internal)\n    @_transparent\n    func _assignIfNotEqual<Value: Equatable>(\n        _ newValue: Value,\n        to keyPath: ReferenceWritableKeyPath<Self, Value?>\n    ) {\n        if self[keyPath: keyPath] != newValue {\n            self[keyPath: keyPath] = newValue\n        }\n    }\n}\n    \n@_spi(Internal)\n@_disfavoredOverload\n@_transparent\npublic func _assignIfNotEqual<Value: AnyObject>(\n    _ value: Value,\n    to destination: inout Value\n) {\n    if value !== destination {\n        destination = value\n    }\n}\n\n@_spi(Internal)\n@_disfavoredOverload\n@_transparent\npublic func _assignIfNotEqual<Value: AnyObject>(\n    _ value: Value,\n    to destination: inout Value?\n) {\n    if value !== destination {\n        destination = value\n    }\n}\n\n@_spi(Internal)\n@_transparent\npublic func _assignIfNotEqual<Value: Equatable>(\n    _ value: Value,\n    to destination: inout Any\n) {\n    if let _destination = destination as? Value {\n        if value != _destination {\n            destination = value\n        }\n    } else {\n        destination = value\n    }\n}\n\n@_spi(Internal)\n@_transparent\npublic func _assignIfNotEqual<Value: Equatable>(\n    _ value: Value,\n    to destination: inout Any?\n) {\n    if let _destination = destination as? Value {\n        if value != _destination {\n            destination = value\n        }\n    } else {\n        destination = value\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchResultsView.swift",
    "content": "//\n//  SearchResultsView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 27/06/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nstruct SearchResultsView<Item: Identifiable, Content: View>: View {\n    @ViewBuilder let content: (Item) -> Content\n    let results: [Item]\n    \n    init(\n        results: [Item],\n        @ViewBuilder content: @escaping (Item) -> Content\n    ) {\n        self.results = results\n        self.content = content\n    }\n    \n    var body: some View {\n        ForEach(results) { item in\n            content(item)\n                .padding(.horizontal, Constants.main.standardSpacing)\n                .padding(.bottom, Constants.main.halfSpacing)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchSheetView.swift",
    "content": "//\n//  SearchSheetView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 27/06/2024.\n//\n\nimport Combine\nimport MlemMiddleware\nimport SwiftUI\n\nstruct SearchSheetView<Item: Searchable, Content: View>: View {\n    @Environment(AppState.self) var appState\n    @Environment(NavigationLayer.self) var navigation\n    \n    @ViewBuilder let content: ([Item], NavigationLayer) -> Content\n    let api: ApiClient\n    let filter: ListingType\n    \n    @State var query: String = \"\"\n    @State var results: [Item] = []\n    \n    /// If `api` is `nil`, the active ApiClient will be used.\n    init(\n        api: ApiClient? = nil,\n        filter: ListingType? = nil,\n        @ViewBuilder content: @escaping ([Item], NavigationLayer) -> Content\n    ) {\n        self.api = api ?? AppState.main.firstApi\n        self.filter = filter ?? .all\n        self.content = content\n    }\n    \n    var body: some View {\n        ScrollView {\n            VStack(alignment: .leading, spacing: 0) {\n                content(results, navigation)\n            }\n        }\n        .background(.themedGroupedBackground)\n        .presentationBackground(.themedGroupedBackground)\n        .navigationBarTitleDisplayMode(.inline)\n        .withSheetSearch(query: $query)\n        .task(id: query, priority: .userInitiated) {\n            do {\n                if !query.isEmpty {\n                    try await Task.sleep(for: .seconds(0.2))\n                }\n                let response = try await Item.search(\n                    api: api,\n                    query: query,\n                    page: 1,\n                    limit: 20,\n                    filter: filter,\n                    hostApi: appState.firstApi\n                )\n                Task { @MainActor in\n                    results = response\n                }\n            } catch {\n                handleError(error)\n            }\n        }\n    }\n}\n\nextension SearchSheetView {\n    init<RowContent: View>(\n        api: ApiClient? = nil,\n        filter: ListingType? = nil,\n        @ViewBuilder content: @escaping (Item, NavigationLayer) -> RowContent\n    ) where Content == SearchResultsView<Item, RowContent> {\n        self.api = api ?? AppState.main.firstApi\n        self.filter = filter ?? .all\n        self.content = { (results: [Item], navigation: NavigationLayer) in\n            SearchResultsView(results: results) { item in\n                content(item, navigation)\n            }\n        }\n    }\n    \n    init<RowContent: View, HeaderContent: View>(\n        api: ApiClient? = nil,\n        filter: ListingType? = nil,\n        @ViewBuilder content: @escaping (Item, NavigationLayer) -> RowContent,\n        @ViewBuilder header: @escaping () -> HeaderContent\n    ) where Content == VStack<TupleView<(HeaderContent, SearchResultsView<Item, RowContent>)>> {\n        self.api = api ?? AppState.main.firstApi\n        self.filter = filter ?? .all\n        self.content = { (results: [Item], dismiss: NavigationLayer) in\n            VStack(alignment: .leading, spacing: 0) {\n                header()\n                SearchResultsView(results: results) { item in\n                    content(item, dismiss)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchView+CreatorPicker.swift",
    "content": "//\n//  SearchView+CreatorPicker.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-19.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension SearchView {\n    struct CreatorPicker: View {\n        @Environment(NavigationLayer.self) var navigation\n        \n        let api: ApiClient\n        @Binding var creator: Person?\n        \n        var body: some View {\n            Button(creator?.name ?? .init(localized: \"Anyone\"), icon: .lemmy.person) {\n                if creator == nil {\n                    navigation.openSheet(.personPicker(\n                        api: api,\n                        callback: { person in\n                            creator = person\n                        }\n                    ))\n                } else {\n                    creator = nil\n                }\n            }\n            .buttonStyle(FeedFilterButtonStyle(\n                isOn: creator != nil,\n                icon: creator == nil ? .general.dropDown : .general.close\n            ))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchView+FilterModels.swift",
    "content": "//\n//  SearchView+FilterModels.swift\n//  Mlem\n//\n//  Created by Sjmarf on 04/10/2024.\n//\n\nimport Icons\nimport MlemBackend\nimport MlemMiddleware\nimport SwiftUI\n\nextension SearchView {\n    enum InstanceFilter: Hashable {\n        case any, local, other(InstanceSummary)\n        \n        var label: String {\n            switch self {\n            case .any: .init(localized: \"Any Instance\")\n            case .local: AppState.main.firstApi.host\n            case let .other(instance): instance.host\n            }\n        }\n        \n        var isOther: Bool {\n            switch self {\n            case .other: true\n            default: false\n            }\n        }\n    }\n    \n    enum LocationFilter: Hashable {\n        case any, subscribed, moderated, localInstance, instance(InstanceSummary), community(Community)\n        \n        var label: String {\n            switch self {\n            case .any:\n                .init(localized: \"Anywhere\")\n            case .subscribed:\n                .init(localized: \"Subscribed\")\n            case .moderated:\n                .init(localized: \"Moderated\")\n            case .localInstance:\n                AppState.main.firstApi.host\n            case let .instance(instance):\n                instance.host\n            case let .community(community):\n                community.name\n            }\n        }\n        \n        var icon: Icon {\n            switch self {\n            case .any: .general.website\n            case .subscribed: .lemmy.subscribedFeed\n            case .moderated: .lemmy.moderation\n            case .localInstance, .instance: .lemmy.instance\n            case .community: .lemmy.community\n            }\n        }\n        \n        var isInstance: Bool {\n            switch self {\n            case .instance: true\n            default: false\n            }\n        }\n        \n        var instanceStub: InstanceStub? {\n            if case let .instance(instance) = self {\n                return instance.instanceStub.asLocal()\n            }\n            return nil\n        }\n        \n        var isCommunity: Bool {\n            switch self {\n            case .community: true\n            default: false\n            }\n        }\n    }\n    \n    @Observable\n    class CommunityFilters {\n        var sort: SearchSortType\n        var instance: InstanceFilter = .any\n        \n        init(software: SiteSoftware) {\n            if software.supports(.searchSortType(.top(.allTime))) {\n                self.sort = .top(.allTime)\n            } else {\n                self.sort = .top(.limited(.month))\n            }\n        }\n    }\n    \n    @Observable\n    class PersonFilters {\n        var sort: SearchSortType\n        var instance: InstanceFilter = .any\n        \n        init(software: SiteSoftware) {\n            if software.supports(.searchSortType(.top(.allTime))) {\n                self.sort = .top(.allTime)\n            } else {\n                self.sort = .top(.limited(.month))\n            }\n        }\n    }\n    \n    @Observable\n    class InstanceFilters {\n        var sort: InstanceSort = .score\n    }\n    \n    @Observable\n    class PostFilters {\n        var sort: PostSortType\n        var creator: Person?\n        var location: LocationFilter = .any\n        \n        init(software: SiteSoftware) {\n            if software.supports(.searchSortType(.top(.allTime))) {\n                self.sort = .top(.allTime)\n            } else {\n                self.sort = .top(.limited(.month))\n            }\n        }\n    }\n    \n    @Observable\n    class CommentFilters {\n        var sort: CommentSortType = .top(.allTime)\n        var creator: Person?\n        var location: LocationFilter = .any\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchView+FiltersView.swift",
    "content": "//\n//  SearchView+FiltersView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/09/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension SearchView {\n    @ViewBuilder\n    var filtersView: some View {\n        VStack(alignment: .leading, spacing: 0) {\n            ScrollView(.horizontal) {\n                HStack {\n                    switch selectedTab {\n                    case .communities:\n                        communityFiltersView\n                    case .people:\n                        personFiltersView\n                    case .instances:\n                        instanceFiltersView\n                    case .posts:\n                        postFiltersView\n                    case .comments:\n                        commentFiltersView\n                    }\n                }\n                .padding(.bottom, 12)\n                .padding(.horizontal, Constants.main.standardSpacing)\n            }\n            .scrollIndicators(.hidden)\n        }\n        .animation(.easeOut(duration: 0.1), value: filterAnimationHashValue)\n    }\n    \n    @ViewBuilder\n    private var communityFiltersView: some View {\n        if let communityFilters {\n            CommunitySearchSortPicker(sort: Binding(\n                get: { communityFilters.sort }, set: { self.communityFilters?.sort = $0 }\n            ))\n            .buttonStyle(.feedFilter(isOn: communityFilters.sort != .top(.allTime)))\n            InstancePicker(\n                filter: Binding(get: { communityFilters.instance }, set: { self.communityFilters?.instance = $0 }),\n                requiredFeature: .searchLocalCommunities\n            )\n            .buttonStyle(.feedFilter(isOn: communityFilters.instance != .any))\n        }\n    }\n    \n    @ViewBuilder\n    private var personFiltersView: some View {\n        if let personFilters {\n            Menu(personFilters.sort.label(timeRangeFormat: .topOnly), icon: personFilters.sort.icon) {\n                Picker(\"Sort\", selection: Binding(\n                    get: { personFilters.sort }, set: { self.personFilters?.sort = $0 }\n                )) {\n                    ForEach(SearchSortType.legacyPersonCases, id: \\.self) { item in\n                        Label(item.label(timeRangeFormat: .topOnly), icon: item.icon)\n                    }\n                }\n            }\n            .buttonStyle(.feedFilter(isOn: personFilters.sort != .top(.allTime)))\n            InstancePicker(\n                filter: Binding(get: { personFilters.instance }, set: { self.personFilters?.instance = $0 }),\n                requiredFeature: .searchLocalPeople\n            )\n            .buttonStyle(.feedFilter(isOn: personFilters.instance != .any))\n        }\n    }\n    \n    @ViewBuilder\n    private var postFiltersView: some View {\n        if let postFilters {\n            FeedSortPicker(sort: Binding(get: { postFilters.sort }, set: { self.postFilters?.sort = $0 }))\n                .buttonStyle(.feedFilter(isOn: postFilters.sort != .top(.allTime)))\n            LocationPicker(filter: Binding(get: { postFilters.location }, set: { self.postFilters?.location = $0 }))\n                .buttonStyle(.feedFilter(isOn: postFilters.location != .any))\n            CreatorPicker(\n                api: postFilters.location.instanceStub?.api ?? appState.firstApi,\n                creator: Binding(get: { postFilters.creator }, set: { self.postFilters?.creator = $0 })\n            )\n        }\n    }\n    \n    @ViewBuilder\n    private var commentFiltersView: some View {\n        Menu(commentFilters.sort.label(timeRangeFormat: .topOnly), icon: commentFilters.sort.icon) {\n            Picker(\"Sort\", selection: $commentFilters.sort) {\n                ForEach(CommentSortType.legacyCases, id: \\.self) { item in\n                    Label(item.label(timeRangeFormat: .topOnly), icon: item.icon)\n                }\n            }\n        }\n        .buttonStyle(.feedFilter(isOn: commentFilters.sort != .top(.allTime)))\n        LocationPicker(filter: $commentFilters.location, requiredFeature: .searchLocalComments)\n            .buttonStyle(.feedFilter(isOn: commentFilters.location != .any))\n        CreatorPicker(\n            api: commentFilters.location.instanceStub?.api ?? appState.firstApi,\n            creator: $commentFilters.creator\n        )\n    }\n    \n    @ViewBuilder\n    private var instanceFiltersView: some View {\n        Menu(\n            instanceFilters.sort.label,\n            icon: instanceFilters.sort.icon\n        ) {\n            Picker(\"Sort\", selection: $instanceFilters.sort) {\n                ForEach(InstanceSort.allCases, id: \\.self) { sort in\n                    Label(sort.label.key, icon: sort.icon)\n                }\n            }\n            .pickerStyle(.inline)\n        }\n        .buttonStyle(.feedFilter(isOn: instanceFilters.sort != .score))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchView+InstancePicker.swift",
    "content": "//\n//  SearchView+InstancePicker.swift\n//  Mlem\n//\n//  Created by Sjmarf on 04/10/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension SearchView {\n    struct InstancePicker: View {\n        @Environment(AppState.self) var appState\n        @Environment(NavigationLayer.self) var navigation\n\n        @Binding var filter: InstanceFilter\n        var requiredFeature: Feature?\n        \n        @State var instanceSupportsRequiredFeature: Bool?\n        \n        var allowActiveAccountLocalInstanceSearch: Bool {\n            if requiredFeature != nil {\n                instanceSupportsRequiredFeature ?? false\n            } else {\n                true\n            }\n        }\n        \n        var body: some View {\n            Menu(filter.label, icon: .lemmy.instance) {\n                Toggle(\n                    \"Any Instance\",\n                    icon: .lemmy.federation,\n                    isOn: .init(get: { filter == .any }, set: { _ in filter = .any })\n                )\n                if allowActiveAccountLocalInstanceSearch {\n                    Toggle(isOn: .init(get: { filter == .local }, set: { _ in filter = .local })) {\n                        Label {\n                            Text(AppState.main.firstApi.host)\n                        } icon: {\n                            SimpleAvatarView(url: AppState.main.firstSession.instance?.avatar, type: .instanceAvatar)\n                        }\n                    }\n                }\n                switch filter {\n                case let .other(instance):\n                    if instance.host != AppState.main.firstApi.host {\n                        Toggle(isOn: .constant(true)) {\n                            Label {\n                                Text(instance.host)\n                            } icon: {\n                                SimpleAvatarView(url: instance.avatar, type: .instanceAvatar)\n                                    .id(instance.avatar)\n                            }\n                        }\n                    } else {\n                        EmptyView()\n                    }\n                default:\n                    EmptyView()\n                }\n                Button(\"Choose Instance...\", icon: .lemmy.instance) {\n                    navigation.openSheet(.instancePicker(callback: { instance in\n                        filter = .other(instance)\n                    }, requiredFeature: requiredFeature))\n                }\n            }\n            .task(id: appState.firstApi) {\n                if let requiredFeature {\n                    do {\n                        instanceSupportsRequiredFeature = try await appState.firstApi.supports(requiredFeature)\n                    } catch {\n                        handleError(error)\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchView+LocationPicker.swift",
    "content": "//\n//  SearchView+CommunityPicker.swift\n//  Mlem\n//\n//  Created by Sjmarf on 04/10/2024.\n//\n\nimport MlemMiddleware\nimport SwiftUI\n\nextension SearchView {\n    struct LocationPicker: View {\n        @Environment(AppState.self) var appState\n        @Environment(NavigationLayer.self) var navigation\n\n        @Binding var filter: LocationFilter\n        var requiredFeature: Feature?\n        \n        var allowActiveAccountLocalInstanceSearch: Bool {\n            if let requiredFeature {\n                appState.firstApi.supports(requiredFeature, defaultValue: false)\n            } else {\n                true\n            }\n        }\n        \n        var body: some View {\n            Menu(filter.label, icon: filter.icon) {\n                Section {\n                    Toggle(\n                        \"Anywhere\",\n                        systemImage: \"globe\",\n                        isOn: .init(get: { filter == .any }, set: { _ in filter = .any })\n                    )\n                }\n                Section {\n                    switch filter {\n                    case let .community(community):\n                        Toggle(isOn: .constant(true)) {\n                            Label {\n                                Text(community.name)\n                            } icon: {\n                                SimpleAvatarView(url: community.avatar, type: .communityAvatar)\n                                    .id(community.avatar)\n                            }\n                        }\n                    default:\n                        EmptyView()\n                    }\n                    Button(\"Choose Community...\", icon: .lemmy.community) {\n                        navigation.openSheet(.communityPicker(callback: { community in\n                            filter = .community(community)\n                        }))\n                    }\n                }\n                Section {\n                    if !((AppState.main.firstSession as? UserSession)?.subscriptions.communities.isEmpty ?? true) {\n                        Toggle(\n                            \"Subscribed\",\n                            icon: .lemmy.subscribedFeed,\n                            isOn: .init(get: { filter == .subscribed }, set: { _ in filter = .subscribed })\n                        )\n                    }\n                    if !((AppState.main.firstSession as? UserSession)?.person?.moderatedCommunities.value?.isEmpty ?? true) {\n                        Toggle(\n                            \"Moderated\",\n                            icon: .lemmy.moderation,\n                            isOn: .init(get: { filter == .moderated }, set: { _ in filter = .moderated })\n                        )\n                    }\n                }\n                Section {\n                    if allowActiveAccountLocalInstanceSearch {\n                        Toggle(isOn: .init(get: { filter == .localInstance }, set: { _ in filter = .localInstance })) {\n                            Label {\n                                Text(AppState.main.firstApi.host)\n                            } icon: {\n                                SimpleAvatarView(url: AppState.main.firstSession.instance?.avatar, type: .instanceAvatar)\n                            }\n                        }\n                    }\n                    switch filter {\n                    case let .instance(instance):\n                        Toggle(isOn: .constant(true)) {\n                            Label {\n                                Text(instance.host)\n                            } icon: {\n                                SimpleAvatarView(url: instance.avatar, type: .instanceAvatar)\n                                    .id(instance.avatar)\n                            }\n                        }\n                    default:\n                        EmptyView()\n                    }\n                    Button(\"Choose Instance...\", icon: .lemmy.instance) {\n                        navigation.openSheet(.instancePicker(callback: { instance in\n                            filter = .instance(instance)\n                        }, requiredFeature: requiredFeature))\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchView+Logic.swift",
    "content": "//\n//  SearchView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 08/09/2024.\n//\n\nimport MlemBackend\nimport MlemMiddleware\nimport SwiftUI\n\nextension SearchView {\n    var availableTabs: [Tab] {\n        var ret: [Tab] = [.communities, .people, .instances, .posts]\n        if appState.firstApi.supports(.commentSearch, defaultValue: false) || selectedTab == .comments {\n            ret.append(.comments)\n        }\n        return ret\n    }\n    \n    func contentChangeTriggerDebouncedRefresh() {\n        let stashedQuery = query\n        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {\n            if query == stashedQuery {\n                contentChangeTriggerRefresh()\n            }\n        }\n    }\n    \n    func contentChangeTriggerRefresh() {\n        editingRecentSearches = false\n        Task {\n            await refresh(clearBeforeRefresh: false)\n        }\n    }\n    \n    func onFilterRefreshHashValueChange() {\n        Task {\n            await refresh(clearBeforeRefresh: selectedTab == .posts || selectedTab == .comments)\n        }\n    }\n    \n    func returnToHome() {\n        if selectedTab == .posts || selectedTab == .comments {\n            selectedTab = .communities\n        }\n        page = .home\n        if !query.isEmpty {\n            query = \"\"\n            Task { await refresh(clearBeforeRefresh: true) }\n        }\n        resultsScrollToTopTrigger.toggle()\n    }\n    \n    func setupFilters() async {\n        guard communityFilters == nil else { return }\n        do {\n            let software = try await appState.firstApi.software\n            communityFilters = .init(software: software)\n            personFilters = .init(software: software)\n            postFilters = .init(software: software)\n        } catch {\n            handleError(error)\n        }\n    }\n    \n    func refresh(clearBeforeRefresh: Bool) async {\n        do {\n            if clearBeforeRefresh {\n                setInstances(.init())\n            }\n            switch selectedTab {\n            case .communities:\n                try await refreshCommunities(clearBeforeRefresh: clearBeforeRefresh)\n            case .people:\n                try await refreshPeople(clearBeforeRefresh: clearBeforeRefresh)\n            case .instances:\n                try await setInstances(MlemStats.main.searchInstances(\n                    query: query,\n                    sort: filtersActive ? instanceFilters.sort : .score\n                ))\n            case .posts:\n                try await refreshPosts(clearBeforeRefresh: clearBeforeRefresh)\n            case .comments:\n                try await refreshComments(clearBeforeRefresh: clearBeforeRefresh)\n            }\n            if lastExecutedQuery[selectedTab] != query {\n                lastExecutedQuery[selectedTab] = query\n            }\n        } catch {\n            handleError(error)\n        }\n    }\n    \n    private func refreshCommunities(clearBeforeRefresh: Bool) async throws {\n        guard let communityFilters else { return }\n        let refreshApi = getRefreshApi(for: communityFilters.instance)\n        await communityLoader.changeApi(\n            to: refreshApi,\n            context: filtersTracker.filterContext,\n            hostApi: refreshApi == appState.firstApi ? nil : appState.firstApi\n        )\n        \n        let defaultSort: SearchSortType\n        if try await refreshApi.supports(.searchSortType(.top(.allTime))) {\n            defaultSort = .top(.allTime)\n        } else {\n            defaultSort = .top(.limited(.month))\n        }\n        \n        try await communityLoader.refresh(\n            query: query,\n            listing: (!filtersActive || communityFilters.instance == .any) ? .all : .local,\n            sort: filtersActive ? communityFilters.sort : defaultSort,\n            clearBeforeRefresh: clearBeforeRefresh\n        )\n    }\n    \n    private func refreshPeople(clearBeforeRefresh: Bool) async throws {\n        guard let personFilters else { return }\n        let refreshApi = getRefreshApi(for: personFilters.instance)\n        await personLoader.changeApi(\n            to: refreshApi,\n            context: filtersTracker.filterContext\n        )\n        \n        let defaultSort: SearchSortType\n        if try await refreshApi.supports(.searchSortType(.top(.allTime))) {\n            defaultSort = .top(.allTime)\n        } else {\n            defaultSort = .top(.limited(.month))\n        }\n        \n        try await personLoader.refresh(\n            query: query,\n            listing: (!filtersActive || personFilters.instance == .any) ? .all : .local,\n            sort: filtersActive ? personFilters.sort : defaultSort,\n            clearBeforeRefresh: clearBeforeRefresh\n        )\n    }\n    \n    private func refreshPosts(clearBeforeRefresh: Bool) async throws {\n        guard let postFilters else { return }\n        guard !query.isEmpty else { return }\n        let refreshApi = getRefreshApi(for: postFilters.location)\n        await postLoader.searchPostFetcher.changeApi(\n            to: refreshApi,\n            context: filtersTracker.filterContext\n        )\n\n        let defaultSort: PostSortType\n        if try await refreshApi.supports(.searchSortType(.top(.allTime))) {\n            defaultSort = .top(.allTime)\n        } else {\n            defaultSort = .top(.limited(.month))\n        }\n\n        postLoader.searchPostFetcher.setSortType(.v3(filtersActive ? postFilters.sort : defaultSort))\n        postLoader.searchPostFetcher.query = query\n        postLoader.searchPostFetcher.creatorId = filtersActive ? postFilters.creator?.id : nil\n        postLoader.searchPostFetcher.communityId = nil\n        postLoader.searchPostFetcher.listing = .all\n        if filtersActive {\n            switch postFilters.location {\n            case .subscribed:\n                postLoader.searchPostFetcher.listing = .subscribed\n            case .moderated:\n                postLoader.searchPostFetcher.listing = .moderated\n            case .localInstance, .instance:\n                postLoader.searchPostFetcher.listing = .local\n            case let .community(community):\n                postLoader.searchPostFetcher.communityId = community.id\n            default:\n                break\n            }\n        }\n        \n        try await postLoader.refresh(clearBeforeRefresh: clearBeforeRefresh)\n    }\n    \n    public func refreshComments(clearBeforeRefresh: Bool) async throws {\n        guard !query.isEmpty else { return }\n        await commentLoader.searchCommentFetcher.changeApi(\n            to: getRefreshApi(for: commentFilters.location)\n        )\n        var listing: ListingType = .all\n        commentLoader.searchCommentFetcher.communityId = nil\n        commentLoader.searchCommentFetcher.creatorId = filtersActive ? commentFilters.creator?.id : nil\n        if filtersActive {\n            switch commentFilters.location {\n            case .subscribed:\n                listing = .subscribed\n            case .moderated:\n                listing = .moderated\n            case .localInstance, .instance:\n                listing = .local\n            case let .community(community):\n                commentLoader.searchCommentFetcher.communityId = community.id\n            default:\n                break\n            }\n        }\n        try await commentLoader.refresh(\n            query: query,\n            listing: listing,\n            sort: .v3(filtersActive ? commentFilters.sort : .top(.allTime)),\n            clearBeforeRefresh: clearBeforeRefresh\n        )\n    }\n    \n    private func getRefreshApi(for filter: InstanceFilter) -> ApiClient {\n        if !filtersActive {\n            appState.firstApi\n        } else {\n            switch filter {\n            case let .other(instance):\n                instance.instanceStub.asLocal().api\n            default:\n                appState.firstApi\n            }\n        }\n    }\n    \n    private func getRefreshApi(for filter: LocationFilter) -> ApiClient {\n        if !filtersActive {\n            appState.firstApi\n        } else {\n            switch filter {\n            case let .instance(instance):\n                instance.instanceStub.asLocal().api\n            default:\n                appState.firstApi\n            }\n        }\n    }\n    \n    func resolvePostFilterCreator() {\n        guard let postFilters else { return }\n        let api = postFilters.location.instanceStub?.api ?? appState.firstApi\n        if let creator = postFilters.creator, api !== creator.api {\n            Task {\n                let stub = PersonStub(api: api, url: creator.actorId.url)\n                do {\n                    postFilters.creator = try await (stub.getPerson())\n                } catch {\n                    handleError(error)\n                }\n            }\n        }\n    }\n    \n    @MainActor\n    func setInstances(_ newValue: [InstanceSummary]) {\n        instances = newValue\n    }\n    \n    var filterAnimationHashValue: Int {\n        var hasher = Hasher()\n        hasher.combine(filtersActive)\n        hasher.combine(communityFilters?.instance.isOther)\n        hasher.combine(selectedTab)\n        return hasher.finalize()\n    }\n    \n    var filterRefreshHashValue: Int {\n        var hasher = Hasher()\n        hasher.combine(filtersActive)\n        hasher.combine(communityFilters?.sort)\n        hasher.combine(communityFilters?.instance)\n        hasher.combine(personFilters?.sort)\n        hasher.combine(personFilters?.instance)\n        hasher.combine(instanceFilters.sort)\n        hasher.combine(postFilters?.sort)\n        hasher.combine(postFilters?.creator?.actorId)\n        hasher.combine(postFilters?.location)\n        hasher.combine(commentFilters.sort)\n        hasher.combine(commentFilters.creator?.actorId)\n        hasher.combine(commentFilters.location)\n        return hasher.finalize()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchView+Views.swift",
    "content": "//\n//  SearchView+Views.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-01.\n//\n\nimport Haptics\nimport SwiftUI\n\nextension SearchView {\n    @ViewBuilder\n    var tabView: some View {\n        HStack {\n            BubblePicker(\n                availableTabs, selected: $selectedTab,\n                label: { $0.label }\n            )\n            .overlay(alignment: .trailing) {\n                LinearGradient(\n                    colors: [Color.clear, palette.groupedBackground.primary],\n                    startPoint: .leading,\n                    endPoint: .trailing\n                )\n                .frame(width: 10)\n            }\n            if page != .home {\n                Button {\n                    hapticManager.play(haptic: .gentleInfo, tier: .low)\n                    filtersActive.toggle()\n                } label: {\n                    Label(\"Filters\", icon: .general.filter)\n                        .symbolVariant(filtersActive ? .fill : .none)\n                        .transaction { $0.animation = nil }\n                }\n                .labelStyle(.iconOnly)\n                .padding(.trailing)\n                .imageScale(.large)\n            }\n        }\n        .animation(.easeOut(duration: 0.1), value: page)\n    }\n    \n    @ViewBuilder\n    var resultsListView: some View {\n        switch selectedTab {\n        case .communities:\n            Group {\n                if query != lastExecutedQuery[.communities] {\n                    ProgressView().padding(.top, 25)\n                } else {\n                    LazyVStack(spacing: 0) {\n                        SearchResultsView(results: communityLoader.items) { community in\n                            CommunityListRow(\n                                community,\n                                readout: .subscribers,\n                                visitContext: page == .home ? .other : .search\n                            )\n                            .onAppear {\n                                do {\n                                    try communityLoader.loadIfThreshold(community)\n                                } catch {\n                                    handleError(error)\n                                }\n                            }\n                        }\n                        EndOfFeedView(feedLoader: communityLoader, viewType: .hobbit)\n                    }\n                }\n            }\n            .animation(.easeOut(duration: 0.1), value: communityLoader.items.isEmpty)\n        case .people:\n            Group {\n                if query != lastExecutedQuery[.people] {\n                    ProgressView().padding(.top, 25)\n                } else {\n                    LazyVStack(spacing: 0) {\n                        SearchResultsView(results: personLoader.items) { person in\n                            PersonListRow(\n                                person,\n                                complications: [.instance, .date],\n                                readout: .postsAndComments,\n                                visitContext: page == .home ? .other : .search\n                            )\n                            .onAppear {\n                                do {\n                                    try personLoader.loadIfThreshold(person)\n                                } catch {\n                                    handleError(error)\n                                }\n                            }\n                        }\n                        EndOfFeedView(feedLoader: personLoader, viewType: .hobbit)\n                    }\n                }\n            }\n            .animation(.easeOut(duration: 0.1), value: personLoader.items.isEmpty)\n        case .instances:\n            Group {\n                if query != lastExecutedQuery[.instances] {\n                    ProgressView().padding(.top, 25)\n                } else {\n                    LazyVStack(spacing: 0) {\n                        SearchResultsView(results: instances) { instance in\n                            InstanceListRow(\n                                instance,\n                                readout: .users,\n                                visitContext: page == .home ? .other : .search\n                            )\n                        }\n                        EndOfFeedView(loadingState: .done, viewType: .hobbit)\n                    }\n                }\n            }\n        case .posts:\n            Group {\n                if postLoader.loadingState == .idle, postLoader.items.isEmpty {\n                    searchPlaceholder\n                } else if query != lastExecutedQuery[.posts] {\n                    ProgressView().padding(.top, 25)\n                } else {\n                    PostGridView(postFeedLoader: postLoader, alwaysShowRead: true)\n                }\n            }\n            .animation(.easeOut(duration: 0.1), value: personLoader.items.isEmpty)\n        case .comments:\n            if commentLoader.loadingState == .idle, commentLoader.items.isEmpty {\n                searchPlaceholder\n            } else if query != lastExecutedQuery[.comments] {\n                ProgressView().padding(.top, 25)\n            } else {\n                VStack(spacing: 0) {\n                    LazyVStack(spacing: compactComments ? Constants.main.halfSpacing : Constants.main.standardSpacing) {\n                        ForEach(commentLoader.items, id: \\.actorId) { comment in\n                            NavigationLink(.comment(comment)) {\n                                FeedCommentView(comment: comment)\n                            }\n                            .buttonStyle(.empty)\n                            .onAppear {\n                                do {\n                                    try commentLoader.loadIfThreshold(comment)\n                                } catch {\n                                    handleError(error)\n                                }\n                            }\n                        }\n                    }\n                    .animation(.easeOut(duration: 0.1), value: commentLoader.items.isEmpty)\n                    .padding(.horizontal, Constants.main.standardSpacing)\n                    EndOfFeedView(feedLoader: commentLoader, viewType: .hobbit)\n                }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    var recentSearchesListView: some View {\n        if let session = appState.firstSession as? UserSession,\n           let visitHistory = session.visitHistory {\n            switch selectedTab {\n            case .communities:\n                let items = visitHistory.communities(withContext: .search)\n                if !items.isEmpty {\n                    recentSearchesHeader\n                    SearchResultsView(results: items) { community in\n                        HStack {\n                            CommunityListRow(community, readout: .subscribers)\n                                .disabled(editingRecentSearches)\n                            deleteRecentSearchButton(session: session) {\n                                visitHistory.removeCommunity(community, context: .search)\n                            }\n                        }\n                    }\n                } else {\n                    searchPlaceholder\n                }\n            case .people:\n                let items = visitHistory.people(withContext: .search)\n                if !items.isEmpty {\n                    recentSearchesHeader\n                    SearchResultsView(results: items) { person in\n                        HStack {\n                            PersonListRow(person, readout: .postsAndComments)\n                                .disabled(editingRecentSearches)\n                            deleteRecentSearchButton(session: session) {\n                                visitHistory.removePerson(person, context: .search)\n                            }\n                        }\n                    }\n                } else {\n                    searchPlaceholder\n                }\n            case .instances:\n                let items = visitHistory.instances(withContext: .search)\n                if !items.isEmpty {\n                    recentSearchesHeader\n                    SearchResultsView(results: items) { instance in\n                        HStack {\n                            InstanceListRow(instance, readout: .users)\n                                .disabled(editingRecentSearches)\n                            deleteRecentSearchButton(session: session) {\n                                visitHistory.removeInstance(instance, context: .search)\n                            }\n                        }\n                    }\n                } else {\n                    searchPlaceholder\n                }\n            default:\n                searchPlaceholder\n            }\n        } else {\n            searchPlaceholder\n        }\n    }\n    \n    @ViewBuilder\n    var recentSearchesHeader: some View {\n        HStack {\n            if editingRecentSearches {\n                ClearRecentSearchesButton()\n            } else {\n                Text(\"Recently Searched\")\n                    .foregroundStyle(.themedPrimary)\n            }\n            \n            Spacer()\n            \n            if editingRecentSearches {\n                Button(\"Done\") {\n                    withAnimation {\n                        editingRecentSearches = false\n                    }\n                }\n            } else {\n                Button(\"Edit\") {\n                    withAnimation {\n                        editingRecentSearches = true\n                    }\n                }\n            }\n        }\n        .font(.callout)\n        .bold()\n        .padding(.horizontal, 15)\n        .padding(.bottom, Constants.main.standardSpacing)\n        .padding(.top, Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    var searchPlaceholder: some View {\n        VStack(spacing: 20) {\n            Image(icon: .general.search)\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .frame(width: 120)\n                .fontWeight(.thin)\n                .foregroundStyle(.themedTertiary)\n            Text(searchPlaceholderTitle)\n                .font(.title2)\n                .fontWeight(.semibold)\n                .foregroundStyle(.themedSecondary)\n                .multilineTextAlignment(.center)\n                .padding(.horizontal, 20)\n        }\n        .padding(.top, 30)\n    }\n\n    var searchPlaceholderTitle: LocalizedStringResource {\n        switch selectedTab {\n        case .communities: \"Search for communities\"\n        case .instances: \"Search for Lemmy instances\"\n        case .people: \"Search for users\"\n        case .posts: \"Search for posts\"\n        case .comments: \"Search for comments\"\n        }\n    }\n\n    struct ClearRecentSearchesButton: View {\n        @Environment(AppState.self) var appState\n        \n        @State var showingConfirmation: Bool = false\n        \n        var body: some View {\n            Button(\"Clear\") {\n                showingConfirmation = true\n            }\n            .confirmationDialog(\n                \"Clear search history?\",\n                isPresented: $showingConfirmation,\n                titleVisibility: .visible\n            ) {\n                Button(\"Clear\", role: .destructive) {\n                    if let session = appState.firstSession as? UserSession, let visitHistory = session.visitHistory {\n                        visitHistory.clear()\n                        Task {\n                            do {\n                                try await session.saveVisitHistory()\n                            } catch {\n                                handleError(error)\n                            }\n                        }\n                    }\n                }\n                Button(\"Turn Off Search History\", role: .destructive) {\n                    if let session = appState.firstSession as? UserSession {\n                        Task { @MainActor in\n                            do {\n                                try await session.setVisitHistoryEnabled(false)\n                            } catch {\n                                handleError(error)\n                            }\n                        }\n                    }\n                }\n                Button(\"Cancel\", role: .cancel) {}\n            } message: {\n                Text(\"You can also turn off search history completely for this account.\")\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func deleteRecentSearchButton(session: UserSession, callback: @escaping (() -> Void)) -> some View {\n        if editingRecentSearches {\n            Button(\"Remove Recent Search\", icon: .general.delete) {\n                withAnimation {\n                    callback()\n                }\n                Task(priority: .background) {\n                    try await session.saveVisitHistory()\n                }\n            }\n            .labelStyle(.iconOnly)\n            .foregroundStyle(palette.negative)\n            .padding(.horizontal, Constants.main.halfSpacing)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/SearchView.swift",
    "content": "//\n//  SearchView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 06/07/2024.\n//\n\nimport Haptics\nimport MlemMiddleware\nimport MlemBackend\nimport SwiftUI\nimport Theming\n\nstruct SearchView: View {\n    enum Page {\n        case home, recents, results\n    }\n    \n    enum Tab: CaseIterable, Identifiable {\n        case communities, people, instances, posts, comments\n        \n        var id: Self { self }\n        \n        var label: LocalizedStringResource {\n            switch self {\n            case .communities: \"Communities\"\n            case .people: \"Users\"\n            case .instances: \"Instances\"\n            case .posts: \"Posts\"\n            case .comments: \"Comments\"\n            }\n        }\n        \n        var shouldAutocorrect: Bool {\n            switch self {\n            case .comments, .posts: true\n            case .communities, .people, .instances: false\n            }\n        }\n    }\n    \n    @Environment(AppState.self) var appState\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(FiltersTracker.self) var filtersTracker\n    @Environment(\\.palette) var palette\n    \n    @Setting(\\.comment_compact) var compactComments\n    \n    @State var searchBarFocused: Bool = false\n    @State var isSearching: Bool = false\n    @State var query: String = \"\"\n    @State var hasAppeared: Bool = false\n    @State var page: Page = .home\n    \n    @State var filtersActive: Bool = false\n    @State var communityFilters: CommunityFilters?\n    @State var personFilters: PersonFilters?\n    @State var instanceFilters: InstanceFilters = .init()\n    @State var postFilters: PostFilters?\n    @State var commentFilters: CommentFilters = .init()\n    \n    @State var selectedTab: Tab = .communities\n    @State var resultsScrollToTopTrigger: Bool = false\n    \n    @State var communityLoader: CommunityFeedLoader\n    @State var personLoader: PersonFeedLoader\n    @State var instances: [InstanceSummary] = []\n    @State var postLoader: SearchPostFeedLoader\n    @State var commentLoader: SearchCommentFeedLoader\n    \n    @State var editingRecentSearches: Bool = false\n    @State var lastExecutedQuery: [Tab: String] = .init()\n    \n    init(appState: AppState = .main) {\n        self._communityLoader = .init(wrappedValue: .init(api: appState.firstApi))\n        self._personLoader = .init(wrappedValue: .init(api: appState.firstApi))\n        self._postLoader = .init(\n            wrappedValue: .init(\n                api: appState.firstApi,\n                sortType: .v3(.top(.allTime)),\n                prefetchingConfiguration: .forPostSize(Settings.get(\\.post_size)),\n                urlCache: Constants.main.urlCache\n            )\n        )\n        self._commentLoader = .init(wrappedValue: .init(api: appState.firstApi))\n    }\n    \n    var body: some View {\n        content\n            .background(ThemedColor.themedGroupedBackground)\n            .themedGroupedBackground()\n            .navigationTitle(\"Search\")\n            .navigationBarTitleDisplayMode(.large)\n            .navigationSearchBar(searchBar)\n            .autocorrectionDisabled(!selectedTab.shouldAutocorrect)\n            .navigationSearchBarHiddenWhenScrolling(false)\n            .toolbar { PasteLinkButtonView() }\n            .scrollDismissesKeyboard(.interactively)\n            .onChange(of: query) { _, newValue in\n                switch newValue {\n                case let str where str.hasPrefix(\"@\"), let str where str.hasPrefix(\"!\"):\n                    selectedTab = str.hasPrefix(\"@\") ? .people : .communities\n                    query = \"\"\n                    page = .recents\n                    searchBarFocused = true\n                    contentChangeTriggerDebouncedRefresh()\n                    \n                default:\n                    if page != .home {\n                        page = query.isEmpty ? .recents : .results\n                    }\n                }\n                lastExecutedQuery[selectedTab] = query\n            }\n            .onChange(of: isSearching) {\n                if isSearching, query.isEmpty {\n                    page = .recents\n                }\n            }\n            // Don't use `.task` here, because it triggers when navigating back\n            .onChange(of: query, initial: true) { oldValue, newValue in\n                if oldValue != newValue || selectedTab == .communities && communityLoader.items.isEmpty && !isSearching {\n                    contentChangeTriggerDebouncedRefresh()\n                }\n            }\n            .onChange(of: selectedTab) {\n                contentChangeTriggerRefresh()\n            }\n            .onChange(of: filterRefreshHashValue, onFilterRefreshHashValueChange)\n            .onChange(of: postFilters?.location.instanceStub) {\n                resolvePostFilterCreator()\n            }\n            .onDisappear {\n                editingRecentSearches = false\n            }\n            .environment(\\.feedContext, .search)\n            .onChange(of: appState.firstApi) {\n                communityFilters = nil\n                personFilters = nil\n                postFilters = nil\n            }\n            .task(id: appState.firstApi) { await setupFilters() }\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        FancyScrollView(scrollToTopTrigger: $resultsScrollToTopTrigger) { searchBarFocused = true } content: {\n            VStack(alignment: .leading, spacing: 0) {\n                if page != .home {\n                    tabView\n                    if filtersActive {\n                        filtersView\n                    }\n                }\n            }\n            .padding(.top, -8)\n            switch page {\n            case .recents:\n                recentSearchesListView\n            case .home:\n                SearchHomeView()\n                    .frame(maxWidth: .infinity)\n            case .results:\n                resultsListView\n            }\n        }\n        .animation(.easeOut(duration: 0.1), value: filtersActive)\n        .animation(.easeOut(duration: 0.2), value: page)\n    }\n    \n    func searchBar() -> SearchBar {\n        SearchBar(\n            \"Search...\",\n            text: $query,\n            isEditing: $isSearching,\n            onCommit: {\n                if selectedTab == .posts || selectedTab == .comments {\n                    Task { @MainActor in\n                        await refresh(clearBeforeRefresh: true)\n                    }\n                }\n            }\n        )\n        .returnKeyType(.search)\n        .showsCancelButton(page != .home)\n        .onCancel(perform: returnToHome)\n        .focused($searchBarFocused)\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment(api: .realistic)) {\n//        @Previewable @Environment(AppState.self) var appState\n//        NavigationStack {\n//            SearchView(appState: appState)\n//        }\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/Searchable.swift",
    "content": "//\n//  Searchable.swift\n//  Mlem\n//\n//  Created by Sjmarf on 28/06/2024.\n//\n\nimport MlemBackend\nimport MlemMiddleware\n\n// swiftlint:disable function_parameter_count\nprotocol Searchable: Identifiable {\n    static func search(\n        api: ApiClient,\n        query: String,\n        page: Int,\n        limit: Int,\n        filter: ListingType,\n        hostApi: ApiClient?\n    ) async throws -> [Self]\n}\n\nextension Community: Searchable {\n    static func search(\n        api: ApiClient,\n        query: String,\n        page: Int,\n        limit: Int,\n        filter: ListingType,\n        hostApi: ApiClient?\n    ) async throws -> [Community] {\n        try await api.searchCommunities(query: query, page: page, limit: limit, filter: filter, hostApi: hostApi)\n    }\n}\n\nextension Person: Searchable {\n    static func search(\n        api: ApiClient,\n        query: String,\n        page: Int,\n        limit: Int,\n        filter: ListingType,\n        hostApi: ApiClient? = nil\n    ) async throws -> [Person] {\n        try await api.searchPeople(query: query, page: page, limit: limit, filter: filter)\n    }\n}\n\nextension InstanceSummary: Searchable {\n    static func search(\n        api _: ApiClient,\n        query: String,\n        page _: Int,\n        limit _: Int,\n        filter _: ListingType,\n        hostApi: ApiClient? = nil\n    ) async throws -> [InstanceSummary] {\n        try await MlemStats.main.searchInstances(query: query)\n    }\n}\n\n// swiftlint:enable function_parameter_count\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/VisitHistory+CodedData.swift",
    "content": "//\n//  VisitHistory+CodedData.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-01.\n//\n\nimport Foundation\nimport MlemBackend\nimport MlemMiddleware\n\nextension VisitHistory {\n    struct CodedData: Codable {\n        var communities: [VisitContext: [CodedVisitRecord<Community.CodedData>]] = [:]\n        var people: [VisitContext: [CodedVisitRecord<Person.CodedData>]] = [:]\n        var instances: [VisitContext: [CodedVisitRecord<InstanceSummary>]] = [:]\n    }\n    \n    struct CodedVisitRecord<T: Codable>: Codable {\n        let value: T\n        let date: Date\n    }\n    \n    convenience init(data: CodedData, api: ApiClient) async throws {\n        let communityRecords = try await data.communities.mapValueArraysAsync { item in\n            try await VisitRecord<Community>(value: api.decodeCommunity(item.value), date: item.date)\n        }\n        let personRecords = try await data.people.mapValueArraysAsync { item in\n            try await VisitRecord<Person>(value: api.decodePerson(item.value), date: item.date)\n        }\n        self.init(\n            communityRecords: communityRecords,\n            personRecords: personRecords,\n            instanceRecords: data.instances.mapValues {\n                $0.map { .init(value: $0.value, date: $0.date) }\n            }\n        )\n    }\n    \n    func codedData() async throws -> CodedData {\n        let communities = try await communityRecords.mapValueArraysAsync { item in\n            try await CodedVisitRecord<Community.CodedData>(value: item.value.codedData(), date: item.date)\n        }\n        \n        let people = try await personRecords.mapValueArraysAsync { item in\n            try await CodedVisitRecord<Person.CodedData>(value: item.value.codedData(), date: item.date)\n        }\n        return .init(\n            communities: communities,\n            people: people,\n            instances: instanceRecords.mapValues {\n                $0.map { .init(value: $0.value, date: $0.date) }\n            }\n        )\n    }\n}\n\nprivate func decodeDictionary<InputValue, OutputValue>(\n    _ input: [VisitHistory.VisitContext: [InputValue]],\n    _ transform: (InputValue) async throws -> OutputValue\n) async throws -> [VisitHistory.VisitContext: [OutputValue]] {\n    var output: [VisitHistory.VisitContext: [OutputValue]] = [:]\n    for (context, items) in input {\n        var outputValues: [OutputValue] = []\n        for item in items {\n            try await outputValues.append(transform(item))\n        }\n        output[context] = outputValues\n    }\n    return output\n}\n\nprivate extension Dictionary where Value: Collection, Key == VisitHistory.VisitContext {\n    func mapValueArraysAsync<OutputValue>(\n        _ transform: (Value.Element) async throws -> OutputValue\n    ) async throws -> [Key: [OutputValue]] {\n        var output: [Key: [OutputValue]] = [:]\n        for (context, items) in self {\n            var outputValues: [OutputValue] = []\n            for item in items {\n                try await outputValues.append(transform(item))\n            }\n            output[context] = outputValues\n        }\n        return output\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Search/VisitHistory.swift",
    "content": "//\n//  VisitHistory.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-01.\n//\n\nimport Foundation\nimport MlemBackend\nimport MlemMiddleware\n\n@Observable\nclass VisitHistory {\n    enum VisitContext: Codable {\n        case search, other\n        \n        var maximumHistorySize: Int {\n            switch self {\n            case .search: 15\n            case .other: 5\n            }\n        }\n    }\n    \n    struct VisitRecord<T> {\n        let value: T\n        let date: Date\n    }\n    \n    private(set) var communityRecords: [VisitContext: [VisitRecord<Community>]]\n    private(set) var personRecords: [VisitContext: [VisitRecord<Person>]]\n    \n    // Using `InstanceSummary` here rather than an `Instance` model because otherwise we'd need\n    // to store full `Instance3` models in order to have access to the site `version`, which means\n    // storing a lot of other unnecessary data.\n    private(set) var instanceRecords: [VisitContext: [VisitRecord<InstanceSummary>]]\n    \n    init(\n        communityRecords: [VisitContext: [VisitRecord<Community>]] = [:],\n        personRecords: [VisitContext: [VisitRecord<Person>]] = [:],\n        instanceRecords: [VisitContext: [VisitRecord<InstanceSummary>]] = [:]\n    ) {\n        self.communityRecords = communityRecords\n        self.personRecords = personRecords\n        self.instanceRecords = instanceRecords\n    }\n    \n    var isEmpty: Bool {\n        communityRecords.isEmpty && personRecords.isEmpty && instanceRecords.isEmpty\n    }\n    \n    func communities(withContext context: VisitContext) -> [Community] {\n        communityRecords[context]?.map(\\.value) ?? []\n    }\n    \n    func communities(withContexts contexts: Set<VisitContext>) -> [Community] {\n        contexts\n            .reduce(into: []) { result, context in\n                result += communityRecords[context] ?? []\n            }\n            .sorted { $0.date > $1.date }\n            .map(\\.value)\n            .uniqued()\n    }\n    \n    func people(withContext context: VisitContext) -> [Person] {\n        personRecords[context]?.map(\\.value) ?? []\n    }\n    \n    func instances(withContext context: VisitContext) -> [InstanceSummary] {\n        instanceRecords[context]?.map(\\.value) ?? []\n    }\n    \n    @MainActor\n    func addCommunity(_ community: Community, context: VisitContext) {\n        addValue(community, to: &communityRecords, context: context)\n    }\n    \n    @MainActor\n    func removeCommunity(_ community: Community, context: VisitContext) {\n        removeValue(community, from: &communityRecords, context: context)\n    }\n    \n    @MainActor\n    func addPerson(_ person: Person, context: VisitContext) {\n        addValue(person, to: &personRecords, context: context)\n    }\n    \n    @MainActor\n    func removePerson(_ person: Person, context: VisitContext) {\n        removeValue(person, from: &personRecords, context: context)\n    }\n    \n    @MainActor\n    func addInstance(_ instance: InstanceSummary, context: VisitContext) {\n        addValue(instance, to: &instanceRecords, context: context)\n    }\n    \n    @MainActor\n    func removeInstance(_ instance: InstanceSummary, context: VisitContext) {\n        removeValue(instance, from: &instanceRecords, context: context)\n    }\n    \n    private func addValue<T: Equatable>(\n        _ value: T,\n        to dict: inout [VisitContext: [VisitRecord<T>]],\n        context: VisitContext\n    ) {\n        removeValue(value, from: &dict, context: context)\n        \n        if !dict.keys.contains(context) {\n            dict[context] = []\n        }\n        dict[context]?.prepend(.init(value: value, date: .now))\n        \n        if dict[context, default: []].count > context.maximumHistorySize {\n            dict[context, default: []].removeLast()\n        }\n    }\n    \n    private func removeValue<T: Equatable>(\n        _ value: T,\n        from dict: inout [VisitContext: [VisitRecord<T>]],\n        context: VisitContext\n    ) {\n        if let index = dict[context, default: []].firstIndex(where: { $0.value == value }) {\n            dict[context, default: []].remove(at: index)\n        }\n    }\n    \n    func clear() {\n        communityRecords = [:]\n        personRecords = [:]\n        instanceRecords = [:]\n    }\n}\n\nextension Set<VisitHistory.VisitContext> {\n    static var all: Set<VisitHistory.VisitContext> {\n        [.other, .search]\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/SelectTextView.swift",
    "content": "//\n//  SelectTextView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 03/03/2024.\n//\n\nimport ComponentViews\nimport Dependencies\nimport Haptics\nimport SwiftUI\nimport SwiftUIIntrospect\n\nstruct SelectTextView: View {\n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.dismiss) var dismiss\n    @Environment(\\.palette) var palette\n    \n    let text: String\n    \n    var body: some View {\n        Group {\n            if #available(iOS 26, *) {\n                ios26Body\n            } else {\n                ios18Body\n            }\n        }\n        .presentationBackgroundInteraction(.enabled)\n    }\n    \n    @ViewBuilder\n    var ios18Body: some View {\n        VStack(spacing: 10) {\n            HStack {\n                Spacer()\n                copyButton\n                    .foregroundStyle(.white)\n                    .frame(height: 30)\n                    .padding(.horizontal, 12)\n                    .background(Capsule().fill(.themedAccent))\n                CloseButtonView()\n            }\n            .padding(.horizontal, 10)\n            textEditor(withBackground: true)\n        }\n        .padding(.top, 10)\n        .presentationCornerRadius(20)\n        .background(.themedBackground)\n    }\n    \n    @available(iOS 26, *)\n    @ViewBuilder\n    var ios26Body: some View {\n        NavigationStack {\n            textEditor(withBackground: false)\n                .padding(.horizontal, 20)\n                .toolbar {\n                    ToolbarItem(placement: .topBarLeading) {\n                        CloseButtonView()\n                    }\n                    ToolbarItem(placement: .topBarTrailing) {\n                        copyButton\n                    }\n                }\n        }\n    }\n    \n    @ViewBuilder\n    func textEditor(withBackground: Bool) -> some View {\n        TextEditor(text: .constant(text))\n            .scrollContentBackground(.hidden)\n            .introspect(.textEditor, on: .iOS(.v17, .v18, .v26)) { textEditor in\n                textEditor.isEditable = false\n                textEditor.textContainerInset = .init(top: 0, left: 10, bottom: 10, right: 10)\n                if withBackground {\n                    textEditor.backgroundColor = UIColor(palette.background.primary)\n                } else {\n                    textEditor.backgroundColor = .clear\n                }\n            }\n    }\n    \n    @ViewBuilder\n    var copyButton: some View {\n        Button {\n            let pasteboard = UIPasteboard.general\n            pasteboard.string = text\n            hapticManager.play(haptic: .lightSuccess, tier: .high)\n            dismiss()\n        } label: {\n            Label(\"Copy All\", icon: .general.copy)\n                .symbolVariant(.fill)\n                .font(.footnote)\n                .fontWeight(.semibold)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ShareInstancePickerView.swift",
    "content": "//\n//  ShareInstancePickerView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-09.\n//\n\nimport ComponentViews\nimport MlemMiddleware\nimport SwiftUI\n\nstruct ShareInstancePickerView: View {\n    @Environment(NavigationLayer.self) var navigation\n    @Environment(\\.dismiss) var dismiss\n    \n    let entity: any Sharable\n    \n    var body: some View {\n        VStack(spacing: 16) {\n            HStack {\n                Text(\"Share using...\")\n                    .fontWeight(.bold)\n                    .foregroundStyle(.themedSecondary)\n                    .padding(.leading, 8)\n                Spacer()\n                closeButton\n            }\n            VStack(spacing: 0) {\n                instanceTargetRow(entity.host, label: \"My Instance\", url: entity.url())\n                Divider()\n                instanceTargetRow(entity.actorId.host, label: \"Original Instance\", url: entity.actorId.url)\n                if let lemmyverseUrl = entity.lemmyverseUrl {\n                    Divider()\n                    instanceTargetRow(\"lemmyverse.link\", label: \"Universal\", url: lemmyverseUrl)\n                }\n            }\n            .frame(maxWidth: .infinity)\n            .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: 16))\n            chooseButtonView\n        }\n        .padding(16)\n        .presentationBackground(.themedGroupedBackground)\n    }\n    \n    @ViewBuilder\n    var closeButton: some View {\n        if #available(iOS 26, *) {\n            Button {\n                dismiss()\n            } label: {\n                Label(\"Close\", icon: .general.close)\n                    .padding(10)\n                    .background(.themedSecondaryGroupedBackground, in: .circle)\n                    .foregroundStyle(.themedSecondary)\n            }\n            .labelStyle(.iconOnly)\n            .font(.title)\n            .buttonStyle(.plain)\n        } else {\n            CloseButtonView()\n        }\n    }\n    \n    @ViewBuilder\n    func instanceTargetRow(_ host: String, label: LocalizedStringResource, url: URL) -> some View {\n        Button {\n            navigation.dismissSheet()\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {\n                NavigationModel.main.shareInfo = .init(url: url, actions: entity.shareSheetActions())\n            }\n        } label: {\n            HStack(spacing: 16) {\n                if host == \"lemmyverse.link\" {\n                    Image(systemName: \"globe\")\n                        .foregroundStyle(.themedAccent)\n                        .frame(width: 42, height: 42)\n                        .background(.themedAccent.opacity(0.2), in: .circle)\n                } else {\n                    CircleCroppedImageView(url: faviconUrl(for: url), frame: 42, fallback: .instanceAvatar)\n                }\n                VStack(alignment: .leading, spacing: 5) {\n                    Text(host)\n                        .foregroundStyle(.themedPrimary)\n                    Text(label)\n                        .font(.footnote)\n                        .foregroundStyle(.themedSecondary)\n                }\n            }\n            .frame(maxWidth: .infinity, alignment: .leading)\n            .padding(.horizontal, 16)\n            .padding(.vertical, 12)\n        }\n    }\n\n    func faviconUrl(for instanceUrl: URL) -> URL? {\n        guard let host = instanceUrl.host() else { return nil }\n        let summary = MlemStats.main.instances?.first(where: { $0.host == host })\n        return summary?.avatar?.withIconSize(128)\n    }\n    \n    @ViewBuilder\n    var chooseButtonView: some View {\n        Button {\n            let model = navigation.model\n            navigation.dismissSheet()\n            guard let model else { return }\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {\n                model.openSheet(.instancePicker(callback: { instance in\n                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {\n                        Task {\n                            await resolveEntity(url: instance.instanceStub.actorId.url, model: model)\n                        }\n                    }\n                }))\n            }\n        } label: {\n            HStack(spacing: 16) {\n                Image(icon: .general.search)\n                    .frame(width: 42)\n                Text(\"Choose Another Instance...\")\n            }\n            .padding(16)\n            .frame(maxWidth: .infinity, alignment: .leading)\n            .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: 16))\n        }\n    }\n    \n    func resolveEntity(url: URL, model: NavigationModel) async {\n        let toastId = ToastModel.main.add(.loading(\"Resolving...\"), location: .bottom)\n        do {\n            let client = ApiClient.getApiClient(url: url, username: nil)\n            let resolvedEntity = try await client.resolve(url: entity.actorId.url)\n            NavigationModel.main.shareInfo = .init(\n                url: resolvedEntity.url(),\n                actions: entity.shareSheetActions()\n            )\n            ToastModel.main.removeToast(id: toastId)\n        } catch {\n            ToastModel.main.removeToast(id: toastId)\n            handleError(error)\n        }\n    }\n}\n\n// TODO: updated mocks\n// #if DEBUG\n//    #Preview(traits: .sampleEnvironment) {\n//        ScrollView {\n//            VStack(spacing: Constants.main.standardSpacing) {\n//                LargePostView(post: Post2.mock(.realistic(.yorkshireDales)))\n//                LargePostView(post: Post2.mock(.realistic(.meguroRiver)))\n//            }\n//            .padding(.horizontal, Constants.main.standardSpacing)\n//        }\n//        .background(.themedGroupedBackground)\n//        .sheet(isPresented: .constant(true)) {\n//            ShareInstancePickerView(entity: Community2.mock(.realistic(.pics)))\n//        }\n//    }\n// #endif\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ShieldsBadgeView/ShieldsBadgeView+Logic.swift",
    "content": "//\n//  ShieldsBadgeView+Logic.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-28.\n//\n\nimport Foundation\n\nextension ShieldsBadgeView {\n    enum LogoType { case bundle(String), system(String) }\n    \n    mutating func decodeBadgeType(_ path: [String]) {\n        switch path[1] {\n        case \"mastodon\":\n            label = .init(localized: \"Follow on Mastodon\")\n            logo = .bundle(\"mastodon.logo\")\n        case \"discord\":\n            label = .init(localized: \"Join Discord Server\")\n            logo = .bundle(\"discord.logo\")\n        case \"matrix\":\n            label = .init(localized: \"Join Matrix Room\")\n            logo = .bundle(\"matrix.logo\")\n        case \"github\":\n            label = \"GitHub\"\n            logo = .bundle(\"github.logo\")\n        case \"opencollective\":\n            label = \"OpenCollective\"\n        case \"liberapay\":\n            label = \"LiberaPay\"\n        case \"mozilla-observatory\":\n            label = .init(localized: \"Mozilla Observatory\")\n        case \"lemmy\":\n            label = path[2]\n        default:\n            break\n        }\n    }\n    \n    mutating func decodeLabel(_ text: String) {\n        let parts = text.replacingOccurrences(of: \"_\", with: \" \").split(separator: \"-\")\n        if parts.count == 3 {\n            label = String(parts[0])\n            message = String(parts[1])\n        } else if parts.count == 2 {\n            label = String(parts[0])\n        }\n    }\n    \n    mutating func decodeLogo(name: String) {\n        switch name {\n        case \"github\":\n            logo = .bundle(\"github.logo\")\n        case \"matrix\":\n            logo = .bundle(\"matrix.logo\")\n        case \"mastodon\":\n            logo = .bundle(\"mastodon.logo\")\n        case \"discord\":\n            logo = .bundle(\"discord.logo\")\n        case \"lemmy\":\n            logo = .bundle(\"lemmy.logo\")\n        default:\n            break\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ShieldsBadgeView/ShieldsBadgeView.swift",
    "content": "//\n//  ShieldsBadgeView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-28.\n//\n\nimport SwiftUI\n\n// https://shields.io/badges\n\nstruct ShieldsBadgeView: View {\n    @Environment(\\.palette) var palette\n    @Environment(\\.openURL) var openURL\n    \n    var label: String\n    var message: String?\n    var link: URL?\n    \n    var logo: LogoType?\n    \n    init(shieldsUrl: URL, link: URL?) {\n        self.link = link\n        \n        self.label = .init(localized: \"Unsupported Badge\")\n        if let host = shieldsUrl.host(), host == \"img.shields.io\" {\n            let path = shieldsUrl.pathComponents\n            if path.count >= 3 {\n                decodeBadgeType(path)\n                decodeLabel(path[2])\n                if let components = URLComponents(url: shieldsUrl, resolvingAgainstBaseURL: false) {\n                    if let parameters = components.queryItems {\n                        for parameter in parameters {\n                            switch parameter.name {\n                            case \"logo\":\n                                if let value = parameter.value {\n                                    decodeLogo(name: value)\n                                }\n                            case \"label\":\n                                if let value = parameter.value {\n                                    self.label = value\n                                }\n                            default:\n                                break\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n    \n    init(label: String, message: String?, link: URL?) {\n        self.label = label\n        self.message = message\n        self.link = link\n    }\n    \n    var body: some View {\n        HStack(spacing: 7) {\n            Group {\n                switch logo {\n                case let .bundle(name):\n                    Image(name)\n                case let .system(systemName):\n                    Image(systemName: systemName)\n                case nil:\n                    EmptyView()\n                }\n                Text(label)\n                    .padding(.vertical, 3)\n            }\n            .foregroundStyle(message != nil ? .themedPrimary : .themedContrastingLabel)\n            if let message {\n                Text(message)\n                    .padding(.vertical, 3)\n                    .padding(.horizontal, 7)\n                    .foregroundStyle(.themedContrastingLabel)\n                    .background(.themedAccent)\n            }\n        }\n        .padding(.leading, 7)\n        .padding(.trailing, message == nil ? 7 : 0)\n        .background(message == nil ? palette.accent : .clear)\n        .clipShape(RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius))\n        .overlay {\n            RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius)\n                .stroke(.themedAccent, lineWidth: 1)\n        }\n        .onTapGesture {\n            if let link {\n                openURL(link)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Toast/Toast.swift",
    "content": "//\n//  Toast.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/05/2024.\n//\n\nimport Foundation\n\nclass Toast: Identifiable, Hashable {\n    let type: ToastType\n    let location: ToastLocation\n    let important: Bool\n    let id: UUID\n    \n    private var killTask: Task<Void, Error>?\n    \n    var killTaskStarted: Bool { killTask != nil }\n    \n    var shouldTimeout: Bool = true {\n        didSet {\n            if oldValue != shouldTimeout {\n                if shouldTimeout {\n                    startKillTask()\n                } else {\n                    killTask?.cancel()\n                    killTask = nil\n                }\n            }\n        }\n    }\n    \n    init(type: ToastType, location: ToastLocation, important: Bool = false) {\n        self.type = type\n        self.location = location\n        self.important = important\n        self.id = .init()\n    }\n    \n    func kill() {\n        ToastModel.main.removeToast(id: id)\n        killTask?.cancel()\n        killTask = nil\n    }\n    \n    func startKillTask() {\n        if shouldTimeout {\n            killTask?.cancel()\n            killTask = Task {\n                try await Task.sleep(\n                    nanoseconds: UInt64(1_000_000_000 * type.duration)\n                )\n                Task { @MainActor in\n                    self.kill()\n                }\n            }\n        }\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(id)\n        hasher.combine(type)\n        hasher.combine(location)\n        hasher.combine(important)\n        hasher.combine(shouldTimeout)\n    }\n    \n    static func == (lhs: Toast, rhs: Toast) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Toast/ToastLocation.swift",
    "content": "//\n//  ToastLocation.swift\n//  Mlem\n//\n//  Created by Sjmarf on 19/05/2024.\n//\n\nimport SwiftUI\n\nenum ToastLocation {\n    case top, bottom\n    \n    var edge: Edge {\n        switch self {\n        case .top:\n            .top\n        case .bottom:\n            .bottom\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Toast/ToastModel.swift",
    "content": "//\n//  ToastModel.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/05/2024.\n//\n\nimport os\nimport SwiftUI\n\n@Observable\nclass ToastModel {\n    private let log: Logger = .mlemLogger()\n    \n    private var toasts: [Toast] = .init()\n    \n    static let main: ToastModel = .init()\n    \n    func activeToasts(location: ToastLocation) -> [Toast] {\n        Array(toasts.filter { $0.location == location }.prefix(3))\n    }\n    \n    @discardableResult\n    func add(_ type: ToastType, location: ToastLocation? = nil, important: Bool? = nil) -> UUID {\n        let newToast: Toast = .init(\n            type: type,\n            location: location ?? type.location,\n            important: important ?? type.important\n        )\n        Task { @MainActor in\n            if !newToast.important, let index = toasts.firstIndex(\n                where: { !$0.important && $0.location == newToast.location }\n            ) {\n                toasts.remove(at: index)\n            }\n            toasts.append(newToast)\n        }\n        return newToast.id\n    }\n    \n    func removeToast(id: UUID) {\n        Task { @MainActor in\n            if let index = toasts.firstIndex(where: { $0.id == id }) {\n                toasts.remove(at: index)\n            } else {\n                log.info(\"No Toast Index\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Toast/ToastOverlayView.swift",
    "content": "//\n//  ToastOverlayView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 17/05/2024.\n//\n\nimport SwiftUI\n\nstruct ToastOverlayView: View {\n    let shouldDisplayNewToasts: Bool\n    let location: ToastLocation\n    \n    @State var activeToasts: [Toast] = []\n    \n    var toastModel: ToastModel { .main }\n    \n    var body: some View {\n        VStack {\n            ForEach(location == .top ? activeToasts : activeToasts.reversed(), id: \\.id) { toast in\n                ToastView(toast: toast)\n                    .transition(\n                        activeToasts.count <= 1 ? .move(edge: location.edge).combined(with: .opacity) : .opacity\n                    )\n                    .onAppear {\n                        toast.startKillTask()\n                    }\n            }\n        }\n        .animation(.snappy(duration: 0.3, extraBounce: 0.2), value: activeToasts)\n        .onChange(of: onChangeHash) {\n            let toasts = toastModel.activeToasts(location: location)\n            if shouldDisplayNewToasts || toasts.isEmpty {\n                activeToasts = toasts\n            }\n        }\n        .onChange(of: shouldDisplayNewToasts) { _, newValue in\n            if !newValue {\n                activeToasts.forEach { $0.kill() }\n                activeToasts = []\n            } else {\n                Task {\n                    try await Task.sleep(nanoseconds: UInt64(100_000_000))\n                    Task { @MainActor in\n                        addNewToasts(toastModel.activeToasts(location: location), startTimersAgain: true)\n                    }\n                }\n            }\n        }\n        .onDisappear {\n            activeToasts.forEach { $0.kill() }\n        }\n        .task {\n            if shouldDisplayNewToasts, activeToasts.isEmpty {\n                do {\n                    try await Task.sleep(nanoseconds: UInt64(500_000_000))\n                    addNewToasts(toastModel.activeToasts(location: location), startTimersAgain: true)\n                } catch {}\n            }\n        }\n    }\n    \n    func addNewToasts(_ toasts: [Toast], startTimersAgain: Bool = true) {\n        for toast in toasts where startTimersAgain || !toast.killTaskStarted {\n            toast.startKillTask()\n        }\n    }\n    \n    var onChangeHash: Int {\n        var hasher = Hasher()\n        hasher.combine(toastModel.activeToasts(location: location).map(\\.id))\n        hasher.combine(shouldDisplayNewToasts)\n        return hasher.finalize()\n    }\n    \n    var taskHash: Int {\n        var hasher = Hasher()\n        hasher.combine(activeToasts.map(\\.id))\n        hasher.combine(activeToasts.map(\\.shouldTimeout))\n        return hasher.finalize()\n    }\n}\n\n#Preview {\n    VStack {\n        Button(String(\"Test\")) {\n            ToastModel.main.add(.success())\n        }\n    }\n    .frame(maxWidth: .infinity, maxHeight: .infinity)\n    .overlay(alignment: .top) {\n        ToastOverlayView(shouldDisplayNewToasts: true, location: .top)\n    }\n    .overlay(alignment: .bottom) {\n        ToastOverlayView(shouldDisplayNewToasts: true, location: .bottom)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Toast/ToastType.swift",
    "content": "//\n//  Toast.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/05/2024.\n//\n\nimport Icons\nimport MlemMiddleware\nimport SwiftUI\nimport Theming\n\nenum ToastType: Hashable {\n    // Don't initialize this directly - use one of the static methods instead\n    case basic(\n        title: String,\n        subtitle: String?,\n        icon: Icon?,\n        color: ThemedColor,\n        duration: Double\n    )\n    \n    static func basic(\n        _ title: LocalizedStringResource,\n        subtitle: LocalizedStringResource? = nil,\n        icon: Icon? = nil,\n        color: ThemedColor? = nil,\n        duration: Double = 1.5\n    ) -> ToastType {\n        let subtitleString: String?\n        if let subtitle {\n            subtitleString = String(localized: subtitle)\n        } else {\n            subtitleString = nil\n        }\n        return .basic(\n            title: String(localized: title),\n            subtitle: subtitleString,\n            icon: icon,\n            color: color ?? .themedAccent,\n            duration: duration\n        )\n    }\n    \n    @_disfavoredOverload\n    static func basic(\n        _ title: some StringProtocol,\n        subtitle: String? = nil,\n        icon: Icon? = nil,\n        color: ThemedColor? = nil,\n        duration: Double = 1.5\n    ) -> ToastType {\n        .basic(\n            title: String(title),\n            subtitle: subtitle,\n            icon: icon,\n            color: color ?? .themedAccent,\n            duration: duration\n        )\n    }\n    \n    // Don't initialize this directly - use one of the static methods instead\n    case undoable(\n        title: String?,\n        icon: Icon?,\n        successIcon: Icon?,\n        callback: () -> Void,\n        color: ThemedColor\n    )\n\n    static func undoable(\n        _ title: LocalizedStringResource? = nil,\n        icon: Icon? = nil,\n        successIcon: Icon? = nil,\n        callback: @escaping () -> Void,\n        color: ThemedColor = .themedAccent\n    ) -> ToastType {\n        let string: String?\n        if let title {\n            string = .init(localized: title)\n        } else {\n            string = nil\n        }\n\n        return .undoable(\n            title: string,\n            icon: icon,\n            successIcon: successIcon,\n            callback: callback,\n            color: color\n        )\n    }\n    \n    @_disfavoredOverload\n    static func undoable(\n        _ title: String? = nil,\n        icon: Icon? = nil,\n        successIcon: Icon? = nil,\n        callback: @escaping () -> Void,\n        color: ThemedColor = .themedAccent\n    ) -> ToastType {\n        .undoable(\n            title: title,\n            icon: icon,\n            successIcon: successIcon,\n            callback: callback,\n            color: color\n        )\n    }\n    \n    case loading(title: String)\n    \n    static func loading(_ title: LocalizedStringResource = \"Loading...\") -> ToastType {\n        .loading(title: String(localized: title))\n    }\n    \n    @_disfavoredOverload\n    static func loading(_ title: String) -> ToastType {\n        .loading(title: title)\n    }\n    \n    static var urlCopyError: ToastType {\n        basic(\n            \"No URL Copied\",\n            subtitle: \"Copy a URL to the clipboard, then try again.\",\n            icon: nil,\n            color: .themedAccent,\n            duration: 2\n        )\n    }\n    \n    case error(_ details: ErrorDetails)\n    \n    case account(any Account)\n    \n    var duration: Double {\n        switch self {\n        case let .basic(_, _, _, _, duration):\n            duration\n        case .undoable:\n            2.5\n        case .account:\n            1.0\n        case .error:\n            Settings.get(\\.dev_errorTimeout)\n        case .loading:\n            10\n        }\n    }\n    \n    var location: ToastLocation {\n        switch self {\n        case .undoable:\n            .bottom\n        default:\n            .top\n        }\n    }\n    \n    var important: Bool {\n        switch self {\n        case .error:\n            true\n        default:\n            false\n        }\n    }\n    \n    static func success(_ message: LocalizedStringResource? = nil) -> Self {\n        if let message {\n            return success(String(localized: message))\n        } else {\n            return success(nil as String?)\n        }\n    }\n    \n    @_disfavoredOverload\n    static func success(_ message: String? = nil) -> Self {\n        .basic(\n            title: message ?? \"Success\",\n            subtitle: nil,\n            icon: .general.success,\n            color: .themedPositive,\n            duration: 1\n        )\n    }\n    \n    static func failure(_ message: LocalizedStringResource? = nil) -> Self {\n        if let message {\n            return failure(String(localized: message))\n        } else {\n            return failure(nil as String?)\n        }\n    }\n    \n    @_disfavoredOverload\n    static func failure(_ message: String? = nil) -> Self {\n        .basic(\n            title: message ?? \"Failed\",\n            subtitle: nil,\n            icon: .general.failure,\n            color: .themedNegative,\n            duration: 1\n        )\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        switch self {\n        case let .basic(title, subtitle, systemImage, color, duration):\n            hasher.combine(\"basic\")\n            hasher.combine(title)\n            hasher.combine(subtitle)\n            hasher.combine(systemImage)\n            hasher.combine(color)\n            hasher.combine(duration)\n        case let .undoable(\n            title: title,\n            icon: icon,\n            successIcon: successIcon,\n            callback: _,\n            color: color\n        ):\n            hasher.combine(\"undoable\")\n            hasher.combine(title)\n            hasher.combine(icon)\n            hasher.combine(successIcon)\n            hasher.combine(color)\n        case let .error(details):\n            hasher.combine(\"error\")\n            hasher.combine(details)\n        case let .account(account):\n            hasher.combine(\"account\")\n            hasher.combine(account)\n        case .loading:\n            hasher.combine(\"loading\")\n        }\n    }\n    \n    static func == (lhs: ToastType, rhs: ToastType) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/Toast/ToastView.swift",
    "content": "//\n//  ToastView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 17/05/2024.\n//\n\nimport ComponentViews\nimport Icons\nimport SwiftUI\nimport Theming\n\nstruct ToastView: View {\n    @Environment(\\.colorScheme) var colorScheme\n    \n    let toast: Toast\n    @State private var isExpanded: Bool = false\n    @State private var didUndo: Bool = false\n    \n    // These symbols only have a single hierarchical layer, so we render it as `.secondary`\n    static let dimmedSymbols: Set<Icon> = [.lemmy.block]\n    \n    // These symbols need `.symbolVariant(.circle.fill)` applied to render properly\n    static let circledSymbols: Set<Icon> = [.general.success, .general.error, .general.failure, .general.undo]\n    \n    var body: some View {\n        HStack {\n            switch toast.type {\n            case let .basic(\n                title: title,\n                subtitle: subtitle,\n                icon: icon,\n                color: color,\n                duration: _\n            ):\n                regularView(\n                    title: title,\n                    subtitle: subtitle,\n                    icon: icon,\n                    imageColor: color\n                )\n            case let .undoable(\n                title: title,\n                icon: icon,\n                successIcon: successIcon,\n                callback: callback,\n                color: color\n            ):\n                Button {\n                    if !didUndo {\n                        didUndo = true\n                        callback()\n                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {\n                            toast.kill()\n                        }\n                    }\n                } label: {\n                    let icon = didUndo ? (successIcon ?? .general.success) : (icon ?? .general.undo)\n                    regularView(\n                        title: title ?? (didUndo ? .init(localized: \"Undone!\") : .init(localized: \"Undo\")),\n                        subtitle: title == nil ? nil : (didUndo ? .init(localized: \"Undone!\") : .init(localized: \"Tap to Undo\")),\n                        icon: icon,\n                        imageColor: color,\n                        subtitleColor: .themedAccent\n                    )\n                    .symbolVariant(ToastView.circledSymbols.contains(icon) ? .circle.fill : .none)\n                    .contentShape(.rect)\n                }\n                .buttonStyle(.empty)\n            case let .error(details):\n                errorView(details)\n            case let .loading(title):\n                loadingView(title)\n            case let .account(account):\n                accountView(account)\n            }\n        }\n        .multilineTextAlignment(.center)\n        .frame(maxHeight: isExpanded ? 230 : nil)\n        .background((colorScheme == .dark ? ThemedColor.themedSecondaryBackground : ThemedColor.themedBackground).opacity(0.5))\n        .background(.regularMaterial)\n        .clipShape(.rect(cornerRadius: 25))\n        .shadow(color: .black.opacity(0.1), radius: 5)\n        .shadow(color: .black.opacity(0.1), radius: 1)\n        .padding(.horizontal)\n    }\n    \n    @ViewBuilder\n    func regularView(\n        title: String,\n        subtitle: String?,\n        icon: Icon?,\n        imageColor: ThemedColor,\n        subtitleColor: ThemedColor = .themedSecondary\n    ) -> some View {\n        HStack(spacing: Constants.main.doubleSpacing) {\n            if let icon {\n                image(icon, color: imageColor)\n                    .symbolVariant(ToastView.circledSymbols.contains(icon) ? .circle.fill : .none)\n                    .contentTransition(.symbolEffect(.replace, options: .speed(4)))\n            }\n            Group {\n                if let subtitle {\n                    VStack(spacing: 1) {\n                        Text(title)\n                            .font(.caption)\n                            .fontWeight(.semibold)\n                            .contentTransition(.opacity)\n                        Text(subtitle)\n                            .font(.caption)\n                            .fontWeight(.semibold)\n                            .foregroundStyle(subtitleColor)\n                            .contentTransition(.opacity)\n                    }\n                    .frame(minWidth: 80)\n                } else {\n                    Text(title)\n                        .lineLimit(1)\n                        .frame(minWidth: 80)\n                }\n            }\n            .padding(icon == nil ? .horizontal : .trailing, Constants.main.doubleSpacing)\n        }\n        .frame(minWidth: 157)\n        .padding(icon == nil ? .vertical : [], Constants.main.standardSpacing)\n    }\n    \n    @ViewBuilder\n    func accountView(_ account: any Account) -> some View {\n        HStack(spacing: Constants.main.doubleSpacing) {\n            CircleCroppedImageView(account, frame: 27, showProgress: false)\n                .padding([.vertical, .leading], Constants.main.standardSpacing)\n            Text(account.nickname)\n                .lineLimit(1)\n                .frame(minWidth: 80)\n                .padding(.trailing, Constants.main.doubleSpacing)\n        }\n        .frame(minWidth: 157)\n    }\n    \n    @ViewBuilder\n    // swiftlint:disable:next function_body_length\n    func errorView(_ details: ErrorDetails) -> some View {\n        Button {\n            if details.error != nil {\n                withAnimation(.bouncy(duration: 0.2)) {\n                    isExpanded = true\n                    toast.shouldTimeout = false\n                }\n            }\n        } label: {\n            let icon = details.icon ?? .general.error\n            VStack(spacing: 0) {\n                HStack {\n                    image(icon, color: .themedNegative)\n                        .symbolVariant(ToastView.circledSymbols.contains(icon) ? .circle.fill : .none)\n                    \n                    Text(details.title ?? .init(localized: \"Error\"))\n                        .frame(minWidth: 100)\n                        .padding(isExpanded ? [] : [.trailing])\n                        .frame(maxWidth: isExpanded ? .infinity : nil)\n                    \n                    if isExpanded {\n                        Button(\"Close\", icon: .general.close) {\n                            toast.kill()\n                        }\n                        .labelStyle(.iconOnly)\n                        .symbolVariant(.circle.fill)\n                        .symbolRenderingMode(.hierarchical)\n                        .foregroundStyle(.themedSecondary)\n                        .foregroundStyle(.secondary)\n                        .font(.title)\n                        .padding(.trailing, 10)\n                    }\n                }\n                .contentShape(.rect)\n                VStack(alignment: .leading, spacing: 0) {\n                    if isExpanded {\n                        ScrollView {\n                            Text(details.errorText())\n                                .foregroundStyle(.red)\n                                .padding(8)\n                                .multilineTextAlignment(.leading)\n                        }\n                        .frame(maxWidth: .infinity)\n                        \n                        Button(\"Copy\", icon: .general.copy) {\n                            UIPasteboard.general.string = details.errorText()\n                        }\n                        .font(.caption)\n                        .buttonStyle(.borderedProminent)\n                        .buttonBorderShape(.capsule)\n                        .tint(.themedNegative)\n                        .padding(Constants.main.standardSpacing)\n                    }\n                }\n                .frame(maxHeight: isExpanded ? .infinity : 0, alignment: .leading)\n                .background(.themedNegative.opacity(isExpanded ? 0.15 : 0))\n            }\n        }\n        .buttonStyle(.empty)\n    }\n    \n    @ViewBuilder\n    func loadingView(_ title: String) -> some View {\n        HStack(spacing: Constants.main.doubleSpacing) {\n            ProgressView()\n                .tint(.themedSecondary)\n                .frame(width: 22, height: 22)\n                .padding([.vertical, .leading], Constants.main.standardSpacing)\n            Text(title)\n                .frame(minWidth: 80)\n                .padding(.trailing, Constants.main.doubleSpacing)\n        }\n        .frame(minWidth: 152)\n    }\n    \n    @ViewBuilder\n    func image(_ icon: Icon, color: ThemedColor) -> some View {\n        Image(icon: icon)\n            .resizable()\n            .aspectRatio(contentMode: .fit)\n            .fontWeight(.semibold)\n            .symbolVariant(.fill)\n            .symbolRenderingMode(.hierarchical)\n            // Don't use palette here! - Sjmarf\n            .foregroundStyle(ToastView.dimmedSymbols.contains(icon) ? .secondary : .primary)\n            .foregroundStyle(color)\n            .frame(width: 27)\n            .padding([.vertical, .leading], Constants.main.standardSpacing)\n    }\n}\n\nextension ToastView {\n    init(_ type: ToastType) {\n        self.init(toast: .init(type: type, location: .top))\n    }\n}\n\n#Preview {\n    VStack {\n        ToastView(.success())\n        ToastView(.failure())\n        ToastView(.undoable(callback: {}))\n        ToastView(.error(.init()))\n        ToastView(.success(String(\"Really super long text\")))\n    }\n    .background {\n        VStack(spacing: 0) {\n            Color.clear\n            HStack(spacing: 0) {\n                Color.red\n                Color.blue\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/ToolbarEllipsisMenu.swift",
    "content": "//\n//  ToolbarEllipsisMenu.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/03/2024.\n//\n\nimport SwiftUI\n\nstruct ToolbarEllipsisMenu<Content: View>: View {\n    let content: Content\n    \n    init(@ViewBuilder content: () -> Content) {\n        self.content = content()\n    }\n    \n    init(_ actions: [any Action]) where Content == ForEach<[any Action], String, MenuButton> {\n        self.init(content: {\n            ForEach(actions, id: \\.id) { action in\n                MenuButton(action: action)\n            }\n        })\n    }\n    \n    var body: some View {\n        Menu {\n            content\n        } label: {\n            Label(\"More\", icon: .general.toolbarMenu)\n                .frame(height: Constants.main.barIconHitbox)\n                .contentShape(Rectangle())\n        }\n        .popupAnchor()\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/WarningOverlayView.swift",
    "content": "//\n//  WarningOverlayView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-30.\n//\n\nimport SwiftUI\n\nstruct WarningOverlayView: View {\n    @Environment(NavigationLayer.self) private var navigation\n    \n    let text: LocalizedStringResource\n    @Binding var isPresented: Bool\n    @Binding var showWarningAgain: Bool\n    \n    var body: some View {\n        VStack(spacing: Constants.main.doubleSpacing) {\n            WarningView(\n                icon: .general.warning,\n                text: text,\n                inList: false\n            )\n            \n            Group {\n                HStack(spacing: Constants.main.doubleSpacing) {\n                    Button {\n                        navigation.pop()\n                    } label: {\n                        Text(\"Go Back\").frame(maxWidth: .infinity)\n                    }\n                    .buttonStyle(.bordered)\n                    \n                    Button {\n                        isPresented = false\n                    } label: {\n                        Text(\"Continue\").frame(maxWidth: .infinity)\n                    }\n                    .buttonStyle(.borderedProminent)\n                }\n                \n                Toggle(isOn: $showWarningAgain.invert(), label: {\n                    Text(\"Don't show this again\")\n                })\n            }\n            .padding(.horizontal, 30)\n        }\n        .padding(Constants.main.doubleSpacing)\n        .background {\n            RoundedRectangle(cornerRadius: Constants.main.largeItemCornerRadius)\n                .fill(.themedBackground.opacity(0.8))\n        }\n        .padding(Constants.main.doubleSpacing)\n        .presentationBackground(.ultraThinMaterial)\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/WarningView.swift",
    "content": "//\n//  WarningView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-19.\n//\n\nimport Foundation\nimport Icons\nimport SwiftUI\nimport Theming\n\nstruct WarningView: View {\n    let icon: Icon\n    let text: String\n    let inList: Bool\n    let overrideColor: ThemedColor?\n    \n    init(icon: Icon, text: LocalizedStringResource, inList: Bool, overrideColor: ThemedColor? = nil) {\n        self.icon = icon\n        self.text = .init(localized: text)\n        self.inList = inList\n        self.overrideColor = overrideColor\n    }\n    \n    @_disfavoredOverload\n    init(icon: Icon, text: some StringProtocol, inList: Bool, overrideColor: ThemedColor? = nil) {\n        self.icon = icon\n        self.text = String(text)\n        self.inList = inList\n        self.overrideColor = overrideColor\n    }\n    \n    var color: ThemedColor { overrideColor ?? .themedWarning }\n    \n    var body: some View {\n        VStack(alignment: .center, spacing: 12) {\n            Image(icon: icon)\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .foregroundStyle(color)\n                .frame(width: 50)\n            Text(text)\n                .font(.headline)\n                .fontWeight(.medium)\n                .multilineTextAlignment(.center)\n                .fixedSize(horizontal: false, vertical: true)\n        }\n        .frame(maxWidth: .infinity)\n        .padding(.vertical, 5)\n        .padding(inList ? 0 : Constants.main.doubleSpacing)\n        .listRowBackground(listBackground())\n        .background(background())\n    }\n    \n    @ViewBuilder\n    func listBackground() -> some View {\n        if inList { backgroundRect }\n    }\n    \n    @ViewBuilder\n    func background() -> some View {\n        if !inList { backgroundRect }\n    }\n    \n    var backgroundRect: some View {\n        RoundedRectangle(cornerRadius: 26)\n            .stroke(color, lineWidth: 3)\n            .background(color.opacity(0.1))\n            .clipShape(RoundedRectangle(cornerRadius: 26))\n    }\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/WebView.swift",
    "content": "//\n//  WebView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 05/02/2024.\n//\n\nimport SwiftUI\nimport WebKit\n\nstruct WebView: UIViewRepresentable {\n    let url: URL\n    \n    func makeUIView(context: Context) -> WKWebView {\n        let wkwebView = WKWebView()\n        let request = URLRequest(url: url)\n        wkwebView.load(request)\n        return wkwebView\n    }\n    \n    func updateUIView(_ uiView: WKWebView, context: Context) {}\n}\n"
  },
  {
    "path": "Mlem/App/Views/Shared/WebsitePreviewView.swift",
    "content": "//\n//  WebsitePreviewView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-16.\n//\n\nimport Foundation\nimport MlemMiddleware\nimport SwiftUI\n\nstruct WebsitePreviewView: View {\n    @Environment(\\.openURL) private var openURL\n    \n    @Setting(\\.post_webPreview_showIcon) var showFavicons\n    @Setting(\\.behavior_muteVideos) var muteVideos\n\n    let shouldBlur: Bool\n    \n    let link: PostLink\n    var onTapActions: (() -> Void)?\n    \n    init(link: PostLink, shouldBlur: Bool, onTapActions: (() -> Void)? = nil) {\n        self.link = link\n        self.onTapActions = onTapActions\n        self.shouldBlur = shouldBlur\n    }\n    \n    var body: some View {\n        content\n            .contentShape(.rect)\n            .onTapGesture {\n                if let onTapActions {\n                    onTapActions()\n                }\n                openURL(link.content)\n            }\n            .contextMenu {\n                Button(\"Open\", icon: .general.browser) {\n                    openURL(link.content)\n                }\n                Button(\"Copy\", icon: .general.copy) {\n                    let pasteboard = UIPasteboard.general\n                    pasteboard.url = link.content\n                }\n                ShareLink(item: link.content)\n            } preview: { WebView(url: link.content) }\n    }\n    \n    var content: some View {\n        complex\n            .frame(maxWidth: .infinity, alignment: .leading)\n            .background(.themedTertiaryGroupedBackground)\n            .clipShape(RoundedRectangle(cornerRadius: Constants.main.mediumItemCornerRadius))\n            .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.mediumItemCornerRadius))\n            .paletteBorder(cornerRadius: Constants.main.mediumItemCornerRadius)\n            .contentShape(.rect)\n    }\n    \n    var complex: some View {\n        VStack(alignment: .leading, spacing: 0) {\n            if let thumbnailUrl = link.effectiveThumbnail {\n                MediaView(\n                    url: thumbnailUrl,\n                    controlState: .constant(.init(\n                        blurred: shouldBlur,\n                        animating: false,\n                        muted: muteVideos\n                    )),\n                    aspectRatioBounds: .bounded(vertical: .init(width: 1, height: 1), horizontal: nil),\n                    contentMode: .fill,\n                    overlays: shouldBlur ? [.controls, .nsfw, .error] : [.controls, .error]\n                )\n                .overlay(alignment: .bottomLeading) {\n                    LinkHostView(link: link, withCapsule: true)\n                        .padding(Constants.main.halfSpacing)\n                }\n            } else {\n                LinkHostView(link: link, withCapsule: false)\n                    .padding([.horizontal, .top], Constants.main.standardSpacing)\n            }\n            \n            Text(link.label)\n                .font(.subheadline)\n                .fontWeight(.semibold)\n                .padding(Constants.main.standardSpacing)\n                .foregroundStyle(.themedPrimary)\n                .fixedSize(horizontal: false, vertical: true)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"aaa.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"dark.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"filename\" : \"tinted.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Default Community.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Lemmy.pdf\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"preserves-vector-representation\" : true\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icon Previews/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icon Previews/icon.eric.lemmy.preview.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Classic Lemmy.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icon Previews/icon.sjmarf.alien.preview.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"logo_small.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icon Previews/icon.sjmarf.green.preview.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"green_small.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icon Previews/icon.sjmarf.ocean.preview.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"logo_small.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icon Previews/icon.sjmarf.orange.preview.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"orange_small.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icon Previews/icon.sjmarf.pink.preview.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"pink_small.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icon Previews/icon.sjmarf.pride.preview.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"light_small.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icon Previews/icon.sjmarf.silver.preview.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"logo_small.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icons/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icons/icon.eric.lemmy.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Classic Lemmy.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icons/icon.sjmarf.alien.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"aaa.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"dark.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"filename\" : \"tinted.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icons/icon.sjmarf.green.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"aaa.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"dark.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"filename\" : \"tinted.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icons/icon.sjmarf.ocean.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"aaa.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"dark.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"filename\" : \"tinted.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icons/icon.sjmarf.orange.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"aaa.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"dark.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"filename\" : \"tinted.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icons/icon.sjmarf.pink.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"aaa.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"dark.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"filename\" : \"tinted.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icons/icon.sjmarf.pride.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"aaa.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"dark.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"filename\" : \"tinted.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Icons/icon.sjmarf.silver.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"aaa.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"dark.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"filename\" : \"tinted.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Symbols/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Symbols/discord.logo.symbolset/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"symbols\" : [\n    {\n      \"filename\" : \"discord-symbol.svg\",\n      \"idiom\" : \"universal\"\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Symbols/github.logo.symbolset/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"symbols\" : [\n    {\n      \"filename\" : \"logo.github.svg\",\n      \"idiom\" : \"universal\"\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Symbols/lemmy.logo.symbolset/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"symbols\" : [\n    {\n      \"filename\" : \"lemmy-symbol.svg\",\n      \"idiom\" : \"universal\"\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Symbols/mastodon.logo.symbolset/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"symbols\" : [\n    {\n      \"filename\" : \"mastodon-symbol.svg\",\n      \"idiom\" : \"universal\"\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/Symbols/matrix.logo.symbolset/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"symbols\" : [\n    {\n      \"filename\" : \"matrix-favicon-symbol.svg\",\n      \"idiom\" : \"universal\"\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/background.earth.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"earth.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/background.trees.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"magbis-amin-vzoCiBC7nK8-unsplash.jpg\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/logo.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"logo.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Assets.xcassets/nsfw.symbolset/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"symbols\" : [\n    {\n      \"filename\" : \"nsfw.svg\",\n      \"idiom\" : \"universal\"\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Info.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>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\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>AppIcon</string>\n\t\t\t<key>CFBundleURLName</key>\n\t\t\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>mlem</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n\t<key>NSAppTransportSecurity</key>\n\t<dict>\n\t\t<key>NSAllowsArbitraryLoads</key>\n\t\t<true/>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "Mlem/Localizable.xcstrings",
    "content": "{\n  \"sourceLanguage\" : \"en\",\n  \"strings\" : {\n    \"**%@** and **%@** chose to defederate from one another.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"**%1$@** and **%2$@** chose to defederate from one another.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"**%1$@** et **%2$@** ont choisi de se défédérer l'une de l'autre.\"\n          }\n        }\n      }\n    },\n    \"**%@** chose to defederate from your instance, **%@**.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"**%1$@** chose to defederate from your instance, **%2$@**.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"**%1$@** a choisi de se défédérer de votre instance, **%2$@**.\"\n          }\n        }\n      }\n    },\n    \"**%@** hasn't chosen to federate with your instance, **%@**.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"**%1$@** hasn't chosen to federate with your instance, **%2$@**.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"**%1$@** n'a pas choisi de se fédérer avec votre instance, **%2$@**.\"\n          }\n        }\n      }\n    },\n    \"%@ Administrator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ Administrateur\"\n          }\n        }\n      }\n    },\n    \"%@ appointed a moderator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a nommé un modérateur\"\n          }\n        }\n      }\n    },\n    \"%@ appointed an administrator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a nommé un administrateur\"\n          }\n        }\n      }\n    },\n    \"%@ banned a user\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a banni un utilisateur\"\n          }\n        }\n      }\n    },\n    \"%@ disallows %@\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"%1$@ disallows %2$@\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$@ interdit %2$@\"\n          }\n        }\n      }\n    },\n    \"%@ has been unresponsive recently.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ n'a pas répondu récemment.\"\n          }\n        }\n      }\n    },\n    \"%@ hid a community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a caché une communauté\"\n          }\n        }\n      }\n    },\n    \"%@ is {{online}}\" : {\n      \"comment\" : \"The word(s) within the curly brackets will be colored green.\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ est {{en ligne}}\"\n          }\n        }\n      }\n    },\n    \"%@ is {{unhealthy}}\" : {\n      \"comment\" : \"The word(s) within the curly brackets will be colored red.\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ n’est {{pas sain}}\"\n          }\n        }\n      }\n    },\n    \"%@ is banned from %@ until %@.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"%1$@ is banned from %2$@ until %3$@.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$@ est banni de %2$@ jusqu'à %3$@.\"\n          }\n        }\n      }\n    },\n    \"%@ is guaranteed by %@.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"%1$@ is guaranteed by %2$@.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$@ est garanti par %2$@.\"\n          }\n        }\n      }\n    },\n    \"%@ is permanently banned from %@.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"%1$@ is permanently banned from %2$@.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$@ est définitivement banni par %2$@.\"\n          }\n        }\n      }\n    },\n    \"%@ locked a post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a verrouillé une publication\"\n          }\n        }\n      }\n    },\n    \"%@ pinned a post to %@\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"%1$@ pinned a post to %2$@\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$@ a épinglé une publication sur %2$@\"\n          }\n        }\n      }\n    },\n    \"%@ purged a comment\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a purgé un commentaire\"\n          }\n        }\n      }\n    },\n    \"%@ purged a community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a purgé une communauté\"\n          }\n        }\n      }\n    },\n    \"%@ purged a post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a purgé une publication\"\n          }\n        }\n      }\n    },\n    \"%@ purged a user\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a purgé un utilisateur\"\n          }\n        }\n      }\n    },\n    \"%@ removed a comment\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a supprimé un commentaire\"\n          }\n        }\n      }\n    },\n    \"%@ removed a community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a supprimé une communauté\"\n          }\n        }\n      }\n    },\n    \"%@ removed a moderator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a supprimé un modérateur\"\n          }\n        }\n      }\n    },\n    \"%@ removed a post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a supprimé une publication\"\n          }\n        }\n      }\n    },\n    \"%@ removed an administrator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a supprimé un administrateur\"\n          }\n        }\n      }\n    },\n    \"%@ restored a comment\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a restauré un commentaire\"\n          }\n        }\n      }\n    },\n    \"%@ restored a community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a restauré une communauté\"\n          }\n        }\n      }\n    },\n    \"%@ restored a post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a restauré une publication\"\n          }\n        }\n      }\n    },\n    \"%@ rules:\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ règles:\"\n          }\n        }\n      }\n    },\n    \"%@ rules...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ règles…\"\n          }\n        }\n      }\n    },\n    \"%@ transferred ownership of a community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a transféré la propriété d'une communauté\"\n          }\n        }\n      }\n    },\n    \"%@ unbanned a user\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a débanni un utilisateur\"\n          }\n        }\n      }\n    },\n    \"%@ unhid a community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a rendu visible une communauté\"\n          }\n        }\n      }\n    },\n    \"%@ unlocked a post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%@ a déverrouillé une publication\"\n          }\n        }\n      }\n    },\n    \"%@ unpinned a post from %@\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"%1$@ unpinned a post from %2$@\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$@ a désépinglé une publication de %2$@\"\n          }\n        }\n      }\n    },\n    \"%lld Active\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%lld actifs\"\n          }\n        }\n      }\n    },\n    \"%lld Crossposts...\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld Crosspost...\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"new\",\n                  \"value\" : \"%lld Crossposts...\"\n                }\n              }\n            }\n          }\n        },\n        \"en-GB\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld Crosspost...\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld Crossposts...\"\n                }\n              }\n            }\n          }\n        },\n        \"fr\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld publication croisée…\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld publications croisées…\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"%lld more links...\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld more link...\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"new\",\n                  \"value\" : \"%lld more links...\"\n                }\n              }\n            }\n          }\n        },\n        \"fr\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld liens en plus…\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld liens en plus…\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"%lld votes\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld vote\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"new\",\n                  \"value\" : \"%lld votes\"\n                }\n              }\n            }\n          }\n        },\n        \"fr\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld vote\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld votes\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"%lld years ago today!\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld year ago today!\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"new\",\n                  \"value\" : \"%lld years ago today!\"\n                }\n              }\n            }\n          }\n        },\n        \"en-GB\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld year ago today!\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"%lld years ago today!\"\n                }\n              }\n            }\n          }\n        },\n        \"fr\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"Il y a %lld an aujourd'hui !\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"Il y a %lld ans aujourd'hui !\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"A censure signifies that an instance disapproves of another instance. Like an endorsement, it is completely subjective and a reason does not have to be given.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Une censure signifie qu'une instance désapprouve une autre instance. Comme une approbation, elle est entièrement subjective et il n'est pas nécessaire de donner de raison.\"\n          }\n        }\n      }\n    },\n    \"A hesitation signifies that an instance mistrusts another instance. It is a milder version of a censure.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Une hésitation signifie qu'une instance se méfie d'une autre instance. C'est une version atténuée d'une censure.\"\n          }\n        }\n      }\n    },\n    \"A selection of communities curated by %@ admins\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Communautés sélectionnées par les administrateurs de %@\\n\"\n          }\n        }\n      }\n    },\n    \"About\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"À propos\"\n          }\n        }\n      }\n    },\n    \"About Mlem\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"À propos de Mlem\"\n          }\n        }\n      }\n    },\n    \"Abuse\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Abus\"\n          }\n        }\n      }\n    },\n    \"Accessibility\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Accessibilité\"\n          }\n        }\n      }\n    },\n    \"Account\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Compte\"\n          }\n        }\n      }\n    },\n    \"Account Created %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Compte crée %@\"\n          }\n        }\n      }\n    },\n    \"Accounts\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Comptes\"\n          }\n        }\n      }\n    },\n    \"Action Type\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Type d’action\"\n          }\n        }\n      }\n    },\n    \"Actions\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Actions\"\n          }\n        }\n      }\n    },\n    \"Active\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Actif\"\n          }\n        }\n      }\n    },\n    \"Active Users\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Utilisateurs actifs\"\n          }\n        }\n      }\n    },\n    \"Add\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter\"\n          }\n        }\n      }\n    },\n    \"Add Account\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter un compte\"\n          }\n        }\n      }\n    },\n    \"Add Administrator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter un administrateur\"\n          }\n        }\n      }\n    },\n    \"Add an image...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter une image...\"\n          }\n        }\n      }\n    },\n    \"Add Comment\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter un commentaire\"\n          }\n        }\n      }\n    },\n    \"Add description\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter une description\"\n          }\n        }\n      }\n    },\n    \"Add Guest\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter un invité\"\n          }\n        }\n      }\n    },\n    \"Add Image\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter image\"\n          }\n        }\n      }\n    },\n    \"Add Language...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter langue\"\n          }\n        }\n      }\n    },\n    \"Add Link\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter lien\"\n          }\n        }\n      }\n    },\n    \"Add Moderator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter un modérateur\"\n          }\n        }\n      }\n    },\n    \"Add Note\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter une note\"\n          }\n        }\n      }\n    },\n    \"Add NSFW Tag\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ajouter le tag NSFW\"\n          }\n        }\n      }\n    },\n    \"Additional Read Indicator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Indicateur supplémentaire de lecture\"\n          }\n        }\n      }\n    },\n    \"Administration\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Administration\"\n          }\n        }\n      }\n    },\n    \"Administrator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Administrateur\"\n          }\n        }\n      }\n    },\n    \"Administrator was appointed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"L'administrateur a été nommé\"\n          }\n        }\n      }\n    },\n    \"Administrator was removed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"L'administrateur a été supprimé\"\n          }\n        }\n      }\n    },\n    \"Advanced\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Avancé\"\n          }\n        }\n      }\n    },\n    \"Alien\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Alien\"\n          }\n        }\n      }\n    },\n    \"Alignment\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Alignement\"\n          }\n        }\n      }\n    },\n    \"All\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Tout\"\n          }\n        }\n      }\n    },\n    \"All Languages\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Toutes les langues\"\n          }\n        }\n      }\n    },\n    \"All Time\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Tout les temps\"\n          }\n        }\n      }\n    },\n    \"Alphabetical\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Alphabétiquement\"\n          }\n        }\n      }\n    },\n    \"Already voted\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Déjà voté\"\n          }\n        }\n      }\n    },\n    \"Always\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Toujours\"\n          }\n        }\n      }\n    },\n    \"Always Allow Direct Loading\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Toujours autoriser le chargement direct\"\n          }\n        }\n      }\n    },\n    \"Always Show Usernames\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Toujours afficher les noms d'utilisateur\"\n          }\n        }\n      }\n    },\n    \"An endorsement signifies that an instance approves of another instance. It is completely subjective, and a reason does not have to be given.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Une approbation signifie qu'une instance approuve une autre instance. Elle est entièrement subjective et il n'est pas nécessaire de donner de raison.\"\n          }\n        }\n      }\n    },\n    \"Animate Avatars...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Animer les avatars...\"\n          }\n        }\n      }\n    },\n    \"Animated Avatars\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Avatars animés\"\n          }\n        }\n      }\n    },\n    \"Anonymous\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Anonyme\"\n          }\n        }\n      }\n    },\n    \"Answer...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Réponse…\"\n          }\n        }\n      }\n    },\n    \"Any\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"N’importe\"\n          }\n        }\n      }\n    },\n    \"Any Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"N’importe quelle communauté\"\n          }\n        }\n      }\n    },\n    \"Any Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"N’importe quelle instance\"\n          }\n        }\n      }\n    },\n    \"Any User\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Tout utilisateur\"\n          }\n        }\n      }\n    },\n    \"Anyone\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"N’importe qui\"\n          }\n        }\n      }\n    },\n    \"Anywhere\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"N’importe où\"\n          }\n        }\n      }\n    },\n    \"App Icon\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Icône de l’application\"\n          }\n        }\n      }\n    },\n    \"Application submitted!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Inscription envoyée !\"\n          }\n        }\n      }\n    },\n    \"Applications\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Inscriptions\"\n          }\n        }\n      }\n    },\n    \"Applications Email Admins\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Administrateurs de messages d’inscription\"\n          }\n        }\n      }\n    },\n    \"Applications Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Inscriptions uniquement\"\n          }\n        }\n      }\n    },\n    \"Apply to All\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Appliquer à tout\"\n          }\n        }\n      }\n    },\n    \"Appoint Administrator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nommer administrateur\"\n          }\n        }\n      }\n    },\n    \"Appoint Moderator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nommer modérateur\"\n          }\n        }\n      }\n    },\n    \"Appointed: %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nommé : %@\"\n          }\n        }\n      }\n    },\n    \"Appointed: %@\\nTo: %@\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Appointed: %1$@\\nTo: %2$@\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nommé : %1$@\\nEn tant que : %2$@\"\n          }\n        }\n      }\n    },\n    \"Approve\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Approuver\"\n          }\n        }\n      }\n    },\n    \"Approved by %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Approuvé par %@\"\n          }\n        }\n      }\n    },\n    \"Are you sure you want to delete all community favorites for this account? This cannot be undone.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Are you sure you want to delete all community favourites for this account? This cannot be undone.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Etes-vous sûr de vouloir supprimer tous les favoris de la communauté pour ce compte ? Cette opération est irréversible.\"\n          }\n        }\n      }\n    },\n    \"Ask Every Time\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Demander à chaque fois\"\n          }\n        }\n      }\n    },\n    \"Ask First\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Demander d'abord\"\n          }\n        }\n      }\n    },\n    \"Ask to confirm every time\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Demander de confirmer à chaque fois\"\n          }\n        }\n      }\n    },\n    \"Authenticating...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Authentification\"\n          }\n        }\n      }\n    },\n    \"Authentication code is incorrect.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le code d’authentification est incorrect.\"\n          }\n        }\n      }\n    },\n    \"Automatic\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Automatique\"\n          }\n        }\n      }\n    },\n    \"Automatically\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Automatiquement\"\n          }\n        }\n      }\n    },\n    \"Automatically enable Reader for supported webpages. You can only enable this when using the in-app browser.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Activer automatiquement le « mode lecture » pour les pages Web prises en charge. Vous ne pouvez activer cette option que lorsque vous utilisez le navigateur intégré à l'application.\"\n          }\n        }\n      }\n    },\n    \"Automatically refreshes every %lld seconds.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"S'actualise automatiquement toutes les %lld secondes.\"\n          }\n        }\n      }\n    },\n    \"Autoplay\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lecture automatique\"\n          }\n        }\n      }\n    },\n    \"Available\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Disponible\"\n          }\n        }\n      }\n    },\n    \"Avatar\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Avatar\"\n          }\n        }\n      }\n    },\n    \"Average: %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Moyenne : %@\"\n          }\n        }\n      }\n    },\n    \"Back\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Retour\"\n          }\n        }\n      }\n    },\n    \"Ban\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bannir\"\n          }\n        }\n      }\n    },\n    \"Ban %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bannir %@\"\n          }\n        }\n      }\n    },\n    \"Ban Duration\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Durée de bannissement\"\n          }\n        }\n      }\n    },\n    \"Ban from Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bannir de la communauté\"\n          }\n        }\n      }\n    },\n    \"Ban from Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bannir de l’instance\"\n          }\n        }\n      }\n    },\n    \"Ban from...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bannir de…\"\n          }\n        }\n      }\n    },\n    \"Ban Target\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cible de bannisement\"\n          }\n        }\n      }\n    },\n    \"Ban User\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bannir l’utilisateur\"\n          }\n        }\n      }\n    },\n    \"Ban...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bannir…\"\n          }\n        }\n      }\n    },\n    \"Banned from Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Banni de la communauté\"\n          }\n        }\n      }\n    },\n    \"Banned from Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Banni de l’Instance\"\n          }\n        }\n      }\n    },\n    \"Banned: %@\\nFrom: %@\\nExpires: %@\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Banned: %1$@\\nFrom: %2$@\\nExpires: %3$@\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Banni : %1$@\\nDepuis : %2$@\\nExpire : %3$@\"\n          }\n        }\n      }\n    },\n    \"Banner\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bannière\"\n          }\n        }\n      }\n    },\n    \"Banning from...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bannissement de...\"\n          }\n        }\n      }\n    },\n    \"Biography\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Biographie\"\n          }\n        }\n      }\n    },\n    \"Block\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bloquer\"\n          }\n        }\n      }\n    },\n    \"Block Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bloquer la communauté\"\n          }\n        }\n      }\n    },\n    \"Block community or user?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bloquer la communauté ou l’utilisateur ?\"\n          }\n        }\n      }\n    },\n    \"Block Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bloquer l'instance\"\n          }\n        }\n      }\n    },\n    \"Block List\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Liste de blocage\"\n          }\n        }\n      }\n    },\n    \"Block User\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bloquer l’utilisateur\"\n          }\n        }\n      }\n    },\n    \"Block...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bloquer…\"\n          }\n        }\n      }\n    },\n    \"Blocked\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bloqué\"\n          }\n        }\n      }\n    },\n    \"Blocked by Cloudflare\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bloqué par Cloudflare\"\n          }\n        }\n      }\n    },\n    \"Blocking...\" : {\n      \"extractionState\" : \"stale\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Blocage…\"\n          }\n        }\n      }\n    },\n    \"Blur\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Flou\"\n          }\n        }\n      }\n    },\n    \"Blur NSFW\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Flouter NSFW\"\n          }\n        }\n      }\n    },\n    \"Blur NSFW Content\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer le contenu NSFW\"\n          }\n        }\n      }\n    },\n    \"Bold\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Gras\"\n          }\n        }\n      }\n    },\n    \"Bot Account\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Compte robot\"\n          }\n        }\n      }\n    },\n    \"Bot accounts are unable to vote.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Les comptes robots ne peuvent pas voter.\"\n          }\n        }\n      }\n    },\n    \"Bottom\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bas\"\n          }\n        }\n      }\n    },\n    \"Browse\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Parcourir\"\n          }\n        }\n      }\n    },\n    \"by %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"par %@\"\n          }\n        }\n      }\n    },\n    \"Bypass Image Proxy\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Contourner le proxy d’image\"\n          }\n        }\n      }\n    },\n    \"Bypass Image Proxy?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Contourner le proxy d’image ?\"\n          }\n        }\n      }\n    },\n    \"Bypass Image Proxy...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Contournement du proxy d’image…\"\n          }\n        }\n      }\n    },\n    \"Cache\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cache\"\n          }\n        }\n      }\n    },\n    \"Cache Cleared\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cache nettoyé\"\n          }\n        }\n      }\n    },\n    \"Cake Day\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Jour du gâteau\"\n          }\n        }\n      }\n    },\n    \"Cancel\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Annuler\"\n          }\n        }\n      }\n    },\n    \"Captcha\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Captcha\"\n          }\n        }\n      }\n    },\n    \"Captcha Difficulty Yes\" : {\n      \"comment\" : \"Used to indicate Captcha difficulty. E.g. \\\"Yes (Hard)\\\".\",\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Yes (%@)\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Oui (%@)\"\n          }\n        }\n      }\n    },\n    \"Censored\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Censuré\"\n          }\n        }\n      }\n    },\n    \"Censured\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Censurés\"\n          }\n        }\n      }\n    },\n    \"Censures\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Censure\"\n          }\n        }\n      }\n    },\n    \"Center\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Centre\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Centré\"\n          }\n        }\n      }\n    },\n    \"Change Password\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Changer le mot de passe\"\n          }\n        }\n      }\n    },\n    \"Change Thumbnail\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Changer la vignette\"\n          }\n        }\n      }\n    },\n    \"Checkmark\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Coche\"\n          }\n        }\n      }\n    },\n    \"Choose a community...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir une communauté...\"\n          }\n        }\n      }\n    },\n    \"Choose a Username\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir un nom d’utilisateur\"\n          }\n        }\n      }\n    },\n    \"Choose Account Type\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir un type de compte\"\n          }\n        }\n      }\n    },\n    \"Choose an action...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir une action…\"\n          }\n        }\n      }\n    },\n    \"Choose another instance...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir une autre instance\"\n          }\n        }\n      }\n    },\n    \"Choose Another Instance...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir une autre instance…\"\n          }\n        }\n      }\n    },\n    \"Choose Community...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sélection de la communauté...\"\n          }\n        }\n      }\n    },\n    \"Choose File\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir un fichier\"\n          }\n        }\n      }\n    },\n    \"Choose how far you have to drag to dismiss the image viewer.\" : {\n\n    },\n    \"Choose Instance...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sélection d’une Instance…\"\n          }\n        }\n      }\n    },\n    \"Choose Language\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir une langue\"\n          }\n        }\n      }\n    },\n    \"Choose target...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir la cible…\"\n          }\n        }\n      }\n    },\n    \"Choose the default sort mode for posts and comments.\" : {\n\n    },\n    \"Choose when Not Safe For Work content should be blurred.\" : {\n\n    },\n    \"Choose when the image viewer controls should appear.\" : {\n\n    },\n    \"Choose whether to show a user's account age next to their username.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir d'afficher ou non l'âge du compte d'un utilisateur à côté de son nom d'utilisateur.\"\n          }\n        }\n      }\n    },\n    \"Choose whether to show a warning when opening a page that is likely to contain sensitive content.\" : {\n\n    },\n    \"Choose whether to show community avatars on posts.\" : {\n\n    },\n    \"Choose whether to use alternate interaction bar and swipe action layouts for post and comment reports in Mod Mail.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir d'utiliser ou non des dispositions de barre d'interaction et d'action de swipe alternatives pour les rapports de publication et de commentaire dans Mod Mail.\"\n          }\n        }\n      }\n    },\n    \"Choose which action to perform when you tap and hold the profile icon.\" : {\n\n    },\n    \"Choose which feed is shown when the app opens.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir le flux à afficher à l'ouverture de l'application.\"\n          }\n        }\n      }\n    },\n    \"Choose which languages appear in your feed. Posts and comments in other languages will be hidden.\" : {\n\n    },\n    \"Choose which widgets to display in your palette.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisir les widgets à afficher dans votre palette.\"\n          }\n        }\n      }\n    },\n    \"Choose wisely - you cannot change this later.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Choisissez judicieusement : vous ne pourrez pas changer cela plus tard.\"\n          }\n        }\n      }\n    },\n    \"Clear\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nettoyer\"\n          }\n        }\n      }\n    },\n    \"Clear Cache\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nettoyer le cache\"\n          }\n        }\n      }\n    },\n    \"Clear Search History\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nettoyer l’historique de recherche\"\n          }\n        }\n      }\n    },\n    \"Clear search history?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nettoyer l’historique de recherche ?\"\n          }\n        }\n      }\n    },\n    \"Click on the link in the email to continue.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cliquez sur le lien dans l'e-mail pour continuer.\"\n          }\n        }\n      }\n    },\n    \"Close\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Fermer\"\n          }\n        }\n      }\n    },\n    \"Close Button\" : {\n\n    },\n    \"Closed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Fermé\"\n          }\n        }\n      }\n    },\n    \"Code\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Code\"\n          }\n        }\n      }\n    },\n    \"Code Block\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bloc de Code\"\n          }\n        }\n      }\n    },\n    \"Collapse\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Déplier\"\n          }\n        }\n      }\n    },\n    \"Collapse Parent\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Déplier le parent\"\n          }\n        }\n      }\n    },\n    \"Collapse Post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Réduire la publication\"\n          }\n        }\n      }\n    },\n    \"Collapse to Top\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Déplier jusqu’en haut\"\n          }\n        }\n      }\n    },\n    \"Color Scheme\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Thème de couleurs\"\n          }\n        }\n      }\n    },\n    \"Comment\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Commentaire\"\n          }\n        }\n      }\n    },\n    \"Comment Downvotes\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votes négatifs sur les commentaires\"\n          }\n        }\n      }\n    },\n    \"Comment Reports\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rapports de commentaires\"\n          }\n        }\n      }\n    },\n    \"Comment Reports Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rapports de commentaires uniquement\"\n          }\n        }\n      }\n    },\n    \"Comment Upvotes\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votes positifs sur les commentaires\"\n          }\n        }\n      }\n    },\n    \"Comment was deleted\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le commentaire a été supprimé\"\n          }\n        }\n      }\n    },\n    \"Comment was purged\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le commentaire a été purgé\"\n          }\n        }\n      }\n    },\n    \"Comment was removed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le commentaire a été retiré\"\n          }\n        }\n      }\n    },\n    \"Comment was restored\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le commentaire a été restauré\"\n          }\n        }\n      }\n    },\n    \"Comments\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Commentaires\"\n          }\n        }\n      }\n    },\n    \"Communities\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Communautés\"\n          }\n        }\n      }\n    },\n    \"Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Communauté\"\n          }\n        }\n      }\n    },\n    \"Community Avatar\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Avatar de le communauté\"\n          }\n        }\n      }\n    },\n    \"Community Creation\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Création de la communauté\"\n          }\n        }\n      }\n    },\n    \"Community Link\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lien de la communauté\"\n          }\n        }\n      }\n    },\n    \"Community ownership was transferred\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La propriété de la communauté a été transférée\"\n          }\n        }\n      }\n    },\n    \"Community was hidden\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La communauté a été cachée\"\n          }\n        }\n      }\n    },\n    \"Community was purged\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La communauté a été purgée\"\n          }\n        }\n      }\n    },\n    \"Community was removed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La communauté a été retirée\"\n          }\n        }\n      }\n    },\n    \"Community was restored\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La communauté a été restaurée\"\n          }\n        }\n      }\n    },\n    \"Community was unhidden\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La communauté a été rendue visible\"\n          }\n        }\n      }\n    },\n    \"Community: %@\\nNew Owner: %@\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Community: %1$@\\nNew Owner: %2$@\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Communauté : %1$@\\nNouveau propriétaire : %2$@\"\n          }\n        }\n      }\n    },\n    \"Compact\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Compacte\"\n          }\n        }\n      }\n    },\n    \"Configure which types of notification should be included in the notification badge.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Configurez les types de notifications à inclure dans le badge de notification.\"\n          }\n        }\n      }\n    },\n    \"Confirm\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Confirmer\"\n          }\n        }\n      }\n    },\n    \"Confirm Image Uploads\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Confirmer le téléversement d’image\"\n          }\n        }\n      }\n    },\n    \"Confirm New Password\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Confirmer le nouveau mot de passe\"\n          }\n        }\n      }\n    },\n    \"Confirm Password\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Conformer le mot de passe\"\n          }\n        }\n      }\n    },\n    \"Connecting...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Connexion…\"\n          }\n        }\n      }\n    },\n    \"Content & Notifications\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Contenu et notifications\"\n          }\n        }\n      }\n    },\n    \"Content Warnings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Avertissements de contenu\"\n          }\n        }\n      }\n    },\n    \"Continue\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Continuer\"\n          }\n        }\n      }\n    },\n    \"Contrast\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Contraste\"\n          }\n        }\n      }\n    },\n    \"Controls\" : {\n\n    },\n    \"Controversial\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Controversé\"\n          }\n        }\n      }\n    },\n    \"Copied\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copié\"\n          }\n        }\n      }\n    },\n    \"Copy\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copier\"\n          }\n        }\n      }\n    },\n    \"Copy a URL to the clipboard, then try again.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copier une URL dans le presse-papiers, et essayer à nouveau.\"\n          }\n        }\n      }\n    },\n    \"Copy All\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Tout copier\"\n          }\n        }\n      }\n    },\n    \"Copy Error\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copier l'erreur\"\n          }\n        }\n      }\n    },\n    \"Copy Name\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copier le nom\"\n          }\n        }\n      }\n    },\n    \"Copy Username\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copier le nom d'utilisateur\"\n          }\n        }\n      }\n    },\n    \"Could not find settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible de trouver les réglages\"\n          }\n        }\n      }\n    },\n    \"Couldn't read URL\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible de lire l’URL\"\n          }\n        }\n      }\n    },\n    \"Counters\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Compteurs\"\n          }\n        }\n      }\n    },\n    \"Create Image\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Créer une image\"\n          }\n        }\n      }\n    },\n    \"Creator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Créateur\"\n          }\n        }\n      }\n    },\n    \"Crosspost\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Publication croisée\"\n          }\n        }\n      }\n    },\n    \"Crossposted from %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Publication croisée depuis %@\"\n          }\n        }\n      }\n    },\n    \"Current Password\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mot de passe actuel\"\n          }\n        }\n      }\n    },\n    \"Current password is incorrect\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le mot de passe actuel est incorrect\"\n          }\n        }\n      }\n    },\n    \"Custom\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Personnalisé\"\n          }\n        }\n      }\n    },\n    \"Custom Order\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Personnaliser l’ordre\"\n          }\n        }\n      }\n    },\n    \"Custom Thumbnail\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vignette personnalisée\"\n          }\n        }\n      }\n    },\n    \"Customize\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise\"\n          }\n        }\n      }\n    },\n    \"Customize Context Menu\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise Context Menu\"\n          }\n        }\n      }\n    },\n    \"Customize how content is displayed in your feed. Choose which types of content are blurred, and apply filters to hide posts from the feed altogether.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise how content is displayed in your feed. Choose which types of content are blurred, and apply filters to hide posts from the feed altogether.\"\n          }\n        }\n      }\n    },\n    \"Customize how moderator actions are separated from regular actions in context menus.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise how moderator actions are separated from regular actions in context menus.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Personnalisez la manière dont les actions du modérateur sont séparées des actions normales dans les menus contextuels.\"\n          }\n        }\n      }\n    },\n    \"Customize how often Mlem plays haptic feedback.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise how often Mlem plays haptic feedback.\"\n          }\n        }\n      }\n    },\n    \"Customize how your subscription list is sorted.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise how your subscription list is sorted.\"\n          }\n        }\n      }\n    },\n    \"Customize Mlem to work best for you. Some features are tied to system-wide accessibility settings.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise Mlem to work best for you. Some features are tied to system-wide accessibility settings.\"\n          }\n        }\n      }\n    },\n    \"Customize the appearance of communities.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise the appearance of communities.\"\n          }\n        }\n      }\n    },\n    \"Customize the appearance of the tab bar.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise the appearance of the tab bar.\"\n          }\n        }\n      }\n    },\n    \"Customize the image viewer's buttons and gestures.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise the image viewer's buttons and gestures.\"\n          }\n        }\n      }\n    },\n    \"Customize the interaction bar for inbox items, and choose which types of notification are included in the tab bar badge.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customise the interaction bar for inbox items, and choose which types of notification are included in the tab bar badge.\"\n          }\n        }\n      }\n    },\n    \"Data not available\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Donnée non disponible\"\n          }\n        }\n      }\n    },\n    \"Days:\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Jours :\"\n          }\n        }\n      }\n    },\n    \"Debug\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débogage\"\n          }\n        }\n      }\n    },\n    \"Default\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Défaut\"\n          }\n        }\n      }\n    },\n    \"Default Feed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Flux par défaut\"\n          }\n        }\n      }\n    },\n    \"Default Feed Type (Desktop)\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Type de flux par défaut (bureau)\"\n          }\n        }\n      }\n    },\n    \"Delete\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer\"\n          }\n        }\n      }\n    },\n    \"Delete Account\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer un compte\"\n          }\n        }\n      }\n    },\n    \"Delete Community Favorites\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Delete Community Favourites\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer les favoris de la communauté\"\n          }\n        }\n      }\n    },\n    \"Delete posts and comments\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer les publications et les commentaires\"\n          }\n        }\n      }\n    },\n    \"Deleted\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimé\"\n          }\n        }\n      }\n    },\n    \"Denied by %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Refusé par %@\"\n          }\n        }\n      }\n    },\n    \"Denied by %@: \\\"%@\\\"\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Denied by %1$@: \\\"%2$@\\\"\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Refusé par %1$@ : « %2$@ »\"\n          }\n        }\n      }\n    },\n    \"Deny\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Refuser\"\n          }\n        }\n      }\n    },\n    \"Deny Application\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Refuser la Demande\"\n          }\n        }\n      }\n    },\n    \"Deprecated Format\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Format obsolète\"\n          }\n        }\n      }\n    },\n    \"Description\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Description\"\n          }\n        }\n      }\n    },\n    \"Details\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Détails\"\n          }\n        }\n      }\n    },\n    \"Developer\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Développeur\"\n          }\n        }\n      }\n    },\n    \"Diagnosing...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Analyse…\"\n          }\n        }\n      }\n    },\n    \"Differentiate Without Color\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Différencier sans couleur\"\n          }\n        }\n      }\n    },\n    \"Disabled\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désactivé\"\n          }\n        }\n      }\n    },\n    \"Discard\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Abandonner\"\n          }\n        }\n      }\n    },\n    \"Disclosure Group\" : {\n      \"extractionState\" : \"stale\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Groupe de divulgation\"\n          }\n        }\n      }\n    },\n    \"Discussion Languages\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Langues de discussion\"\n          }\n        }\n      }\n    },\n    \"Disk Usage\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Utilisation du disque\"\n          }\n        }\n      }\n    },\n    \"Dismiss\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rejeter\"\n          }\n        }\n      }\n    },\n    \"Dismiss Sensitivity\" : {\n\n    },\n    \"Display linked media from supported hosts in-app rather than as a link.\" : {\n\n    },\n    \"Display Name\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nom d'affichage\"\n          }\n        }\n      }\n    },\n    \"Distinguish Interaction Bar\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Distinguer la barre d'interaction\"\n          }\n        }\n      }\n    },\n    \"Divider\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Séparateur\"\n          }\n        }\n      }\n    },\n    \"Domain\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Domaine\"\n          }\n        }\n      }\n    },\n    \"Don't show this again\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ne plus montrer ceci à nouveau\"\n          }\n        }\n      }\n    },\n    \"Done\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Terminé\"\n          }\n        }\n      }\n    },\n    \"Downvote\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vote négatif\"\n          }\n        }\n      }\n    },\n    \"Downvote Counter\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Compteur de vote négatif\"\n          }\n        }\n      }\n    },\n    \"Downvoted\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voté négativement\"\n          }\n        }\n      }\n    },\n    \"Dracula\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dracula\"\n          }\n        }\n      }\n    },\n    \"Easy\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Facile\"\n          }\n        }\n      }\n    },\n    \"Edit\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Éditer\"\n          }\n        }\n      }\n    },\n    \"Edit link\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Modifier le lien\"\n          }\n        }\n      }\n    },\n    \"Edit Note\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Modifier la note\"\n          }\n        }\n      }\n    },\n    \"Edited\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Édité\"\n          }\n        }\n      }\n    },\n    \"Either\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Soit\"\n          }\n        }\n      }\n    },\n    \"Email\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Email\"\n          }\n        }\n      }\n    },\n    \"Email Address\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Adresse email\"\n          }\n        }\n      }\n    },\n    \"Email Verification\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vérification de l'e-mail\"\n          }\n        }\n      }\n    },\n    \"Embedded Content\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Contenu embarqué\"\n          }\n        }\n      }\n    },\n    \"Enable\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Activer\"\n          }\n        }\n      }\n    },\n    \"end.of.feed.icon.1\" : {\n      \"comment\" : \"This is the key for an icon that appears next to the \\\"I think I've found the bottom!\\\" text. It is localized so that you can change the icon to fit better with your translation of the text.\",\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"figure.climbing\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"figure.climbing\"\n          }\n        }\n      }\n    },\n    \"end.of.feed.icon.2\" : {\n      \"comment\" : \"This is the key for an icon that appears next to the \\\"That's all, folks!\\\" text. It is localized so that you can change the icon to fit better with your translation of the text.\",\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"figure.wave\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"figure.wave\"\n          }\n        }\n      }\n    },\n    \"end.of.feed.icon.3\" : {\n      \"comment\" : \"This is the key for an icon that appears next to the \\\"It's turtles all the way down\\\" text. It is localized so that you can change the icon to fit better with your translation of the text.\",\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"tortoise\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"tortoise\"\n          }\n        }\n      }\n    },\n    \"Ended %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Terminé %@\"\n          }\n        }\n      }\n    },\n    \"Endorsements\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Approbations\"\n          }\n        }\n      }\n    },\n    \"Ends %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Se termine %@\"\n          }\n        }\n      }\n    },\n    \"Enter your instance's domain name below.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Saisissez le nom de domaine de votre instance ci-dessous.\"\n          }\n        }\n      }\n    },\n    \"Error\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Erreur\"\n          }\n        }\n      }\n    },\n    \"EULA\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"EULA\"\n          }\n        }\n      }\n    },\n    \"Events\" : {\n\n    },\n    \"Every time I share a link, show a popup asking which instance to use.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"À chaque fois que je partage un lien, une fenêtre contextuelle s'affiche demandant quelle instance utiliser.\"\n          }\n        }\n      }\n    },\n    \"example.com\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"example.com\"\n          }\n        }\n      }\n    },\n    \"Except Applications\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sauf les inscriptions\"\n          }\n        }\n      }\n    },\n    \"Except Comment Reports\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sauf les rapports de commentaires\"\n          }\n        }\n      }\n    },\n    \"Except Mentions\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sauf les mentions\"\n          }\n        }\n      }\n    },\n    \"Except Message Reports\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sauf les rapports de messages\"\n          }\n        }\n      }\n    },\n    \"Except Messages\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sauf les messages\"\n          }\n        }\n      }\n    },\n    \"Except Post Reports\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sauf les rapports de publications\"\n          }\n        }\n      }\n    },\n    \"Except Replies\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sauf les réponses\"\n          }\n        }\n      }\n    },\n    \"Expand\" : {\n\n    },\n    \"Expand Post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Déplier la publication\"\n          }\n        }\n      }\n    },\n    \"Expires:\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Expire :\"\n          }\n        }\n      }\n    },\n    \"Export Settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Paramètres d'exportation\"\n          }\n        }\n      }\n    },\n    \"Export...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Exporter…\"\n          }\n        }\n      }\n    },\n    \"External Links\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Liens externes\"\n          }\n        }\n      }\n    },\n    \"Failed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Échec\"\n          }\n        }\n      }\n    },\n    \"Failed to block!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Échec du blocage !\"\n          }\n        }\n      }\n    },\n    \"Failed to connect to %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible de se connecter à %@\"\n          }\n        }\n      }\n    },\n    \"Failed to delete post!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible de supprimer la publication !\"\n          }\n        }\n      }\n    },\n    \"Failed to delete!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Échec de la suppression !\"\n          }\n        }\n      }\n    },\n    \"Failed to import settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible d’importer les réglages\"\n          }\n        }\n      }\n    },\n    \"Failed to lock post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible de verrouiller la publication\"\n          }\n        }\n      }\n    },\n    \"Failed to open sheet\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible d’ouvrir la page\"\n          }\n        }\n      }\n    },\n    \"Failed to pin post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible d’épingler la publication\"\n          }\n        }\n      }\n    },\n    \"Failed to remove content\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible de supprimer le contenu\"\n          }\n        }\n      }\n    },\n    \"Failed to resolve post. Try another account.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible de résoudre la publication. Essayez un autre compte.\"\n          }\n        }\n      }\n    },\n    \"Failed to restore!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Échec de la restauration !\"\n          }\n        }\n      }\n    },\n    \"Failed to save media\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible de sauvegarder le média\"\n          }\n        }\n      }\n    },\n    \"Failed to set NSFW status\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Échec de la modification du statut NSFW\"\n          }\n        }\n      }\n    },\n    \"Failed to unblock!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Échec du déblocage !\"\n          }\n        }\n      }\n    },\n    \"Failed to unpin post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Impossible de désépingler la publication\"\n          }\n        }\n      }\n    },\n    \"Fallback\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Replis\"\n          }\n        }\n      }\n    },\n    \"Fast\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rapide\"\n          }\n        }\n      }\n    },\n    \"Favorite\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Favourite\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Favoris\"\n          }\n        }\n      }\n    },\n    \"Favorited\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Favourited\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mis en favoris\"\n          }\n        }\n      }\n    },\n    \"Favorites\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Favourites\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Favoris\"\n          }\n        }\n      }\n    },\n    \"Federates\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Fédères\"\n          }\n        }\n      }\n    },\n    \"federation.explanation\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Lemmy instances talk to each other so that content can be shared across sites. This is called \\\"federation\\\". Instance administrators can choose which other instances they would like their instance to federate with. Some instances federate with all but a curated \\\"block-list\\\" of other instances; other instances might only federate with instances on an \\\"allow-list\\\".\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Les instances Lemmy communiquent entre elles afin que le contenu puisse être partagé entre les sites. C'est ce qu'on appelle la « fédération ». Les administrateurs d'instances peuvent choisir avec quelles autres instances ils souhaitent que leur instance se fédère. Certaines instances se fédèrent avec toutes les autres instances, à l'exception d'une « liste de blocage » organisée ; d'autres instances peuvent se fédérer uniquement avec des instances figurant sur une « liste d'autorisation ».\"\n          }\n        }\n      }\n    },\n    \"Fediseer GUI\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Interface Fediseer\"\n          }\n        }\n      }\n    },\n    \"Feed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Flux\"\n          }\n        }\n      }\n    },\n    \"Feed is outdated\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le flux est périmé\"\n          }\n        }\n      }\n    },\n    \"Feeds\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Flux\"\n          }\n        }\n      }\n    },\n    \"Files\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Fichiers\"\n          }\n        }\n      }\n    },\n    \"Filter\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Filtre\"\n          }\n        }\n      }\n    },\n    \"Filter as...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Filtrer en tant que…\"\n          }\n        }\n      }\n    },\n    \"Filter violation\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Violation de filtre\"\n          }\n        }\n      }\n    },\n    \"Filters\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Filtres\"\n          }\n        }\n      }\n    },\n    \"Follow on Mastodon\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Suivre sur Mastodon\"\n          }\n        }\n      }\n    },\n    \"For New Accounts Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Pour les nouveaux comptes seulement\"\n          }\n        }\n      }\n    },\n    \"General\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Général\"\n          }\n        }\n      }\n    },\n    \"Gestures\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Gestes\"\n          }\n        }\n      }\n    },\n    \"GitHub Repository\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dépôt GitHub\"\n          }\n        }\n      }\n    },\n    \"Go Back\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Retour\"\n          }\n        }\n      }\n    },\n    \"Go to Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Aller à l'instance\"\n          }\n        }\n      }\n    },\n    \"Green\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vert\"\n          }\n        }\n      }\n    },\n    \"Grouped\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Groupé\"\n          }\n        }\n      }\n    },\n    \"Guaranteed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Garantis\"\n          }\n        }\n      }\n    },\n    \"Guarantees\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Garanties\"\n          }\n        }\n      }\n    },\n    \"Guest\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Invité\"\n          }\n        }\n      }\n    },\n    \"Haptic Level\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Niveau d’Haptique\"\n          }\n        }\n      }\n    },\n    \"Haptics\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Haptiques\"\n          }\n        }\n      }\n    },\n    \"Hard\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dur\"\n          }\n        }\n      }\n    },\n    \"Heading\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Titre\"\n          }\n        }\n      }\n    },\n    \"Heading %lld\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Titre %lld\"\n          }\n        }\n      }\n    },\n    \"Headline\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"En-tête\"\n          }\n        }\n      }\n    },\n    \"Hesitations\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Hésitations\"\n          }\n        }\n      }\n    },\n    \"Hidden\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masqué\"\n          }\n        }\n      }\n    },\n    \"Hidden by filters\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masqué par les filtres\"\n          }\n        }\n      }\n    },\n    \"Hide\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer\"\n          }\n        }\n      }\n    },\n    \"Hide Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer la communauté\"\n          }\n        }\n      }\n    },\n    \"Hide Details\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer les détails\"\n          }\n        }\n      }\n    },\n    \"Hide links\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cacher le sliens\"\n          }\n        }\n      }\n    },\n    \"Hide Older Incidents\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer les incidents plus vieux\"\n          }\n        }\n      }\n    },\n    \"Hide posts containing certain words, phrases, or character sequences from your feed.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer de votre fil les publications contenant certains mots, expressions ou séquences de caractères.\"\n          }\n        }\n      }\n    },\n    \"Hide posts with titles containing containing these precise character sequences.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer les publications dont les titres contiennent ces séquences exactes de caractères.\"\n          }\n        }\n      }\n    },\n    \"Hide posts with titles containing these whole words or phrases. Ignores case and punctuation (e.g., the keyword \\\"john\\\" will also filter \\\"John's\\\").\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer les publications dont les titres contiennent ces mots ou expressions entiers. Ignore la casse et la ponctuation (par ex., le mot-clé « john » filtrera aussi « John's »).\"\n          }\n        }\n      }\n    },\n    \"Hide Read\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer les lus\"\n          }\n        }\n      }\n    },\n    \"Hide Results\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer les résultats\"\n          }\n        }\n      }\n    },\n    \"Hide Website Icons\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Masquer les icônes du site web\"\n          }\n        }\n      }\n    },\n    \"Hiding Read\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lus masqués\"\n          }\n        }\n      }\n    },\n    \"High\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Élevé\"\n          }\n        }\n      }\n    },\n    \"Highest\" : {\n\n    },\n    \"Hot\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Chaud\"\n          }\n        }\n      }\n    },\n    \"I think I've found the bottom!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Je crois que je suis arrivé en bas !\"\n          }\n        }\n      }\n    },\n    \"If an instance is \\\"guaranteed\\\", it is known as definitely not spam. Unguaranteed instances are not necessarily spam; rather, it is unknown whether a non-guaranteed instance is spam or not.\\n\\nAn instance can be guaranteed by any other guaranteed instance. This forms a chain of guaranteed instances known as the \\\"Chain of Trust\\\". The Chain of Trust starts at the Fediseer itself, which guarantees several of the largest instances.\\n\\nA guarantee can be revoked by the guarantor at any time. If an instance's guarantee is revoked, it returns to a \\\"not guaranteed\\\" state along with any instances it guarantees.\\n\\nOnce an instance has been guaranteed, it is able to express its approval or disapproval of other instances using endorsements, hesitations and censures.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Si une instance est « garantie », elle n'est pas considérée comme du spam. Les instances non garanties ne sont pas nécessairement du spam. En revanche, on ne sait pas si une instance non garantie est du spam ou non.\\n\\nUne instance peut être garantie par n'importe quelle autre instance garantie. Cela forme une chaîne d'instances garanties appelée « chaîne de confiance ». La chaîne de confiance commence au niveau du Fediseer lui-même, qui garantit plusieurs des plus grandes instances.\\n\\nUne garantie peut être révoquée par le garant à tout moment. Si la garantie d'une instance est révoquée, elle revient à l'état « non garantie » avec toutes les instances qu'elle garantit.\\n\\nUne fois qu'une instance a été garantie, elle est en mesure d'exprimer son approbation ou sa désapprobation d'autres instances en utilisant des approbations, des hésitations et des censures.\"\n          }\n        }\n      }\n    },\n    \"If set to \\\"Automatic\\\", the full URL will be hidden in compact comments.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Si défini sur « Automatique », l'URL complète sera masquée dans les commentaires compacts.\"\n          }\n        }\n      }\n    },\n    \"If you are a moderator or administrator of a filtered post, it will appear in your feed but require you to tap to view its content.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Si vous êtes modérateur ou administrateur d'une publication filtrée, elle apparaîtra dans votre fil mais vous devrez appuyer pour en afficher le contenu.\"\n          }\n        }\n      }\n    },\n    \"Image\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Image\"\n          }\n        }\n      }\n    },\n    \"Image loading failed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Échec du chargement de l'image\"\n          }\n        }\n      }\n    },\n    \"Image Saved\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Image enregistrée\"\n          }\n        }\n      }\n    },\n    \"Image Viewer\" : {\n\n    },\n    \"Images are cached on your device for fast reuse. The maximum cache size is around %@.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Les images sont mises en cache sur votre appareil pour une réutilisation rapide. La taille maximale du cache est d'environ %@.\"\n          }\n        }\n      }\n    },\n    \"Immediately\" : {\n\n    },\n    \"Import Settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Paramètres d’importation\"\n          }\n        }\n      }\n    },\n    \"Import...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Importer...\"\n          }\n        }\n      }\n    },\n    \"Import/Export Settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Paramètres d’importation / exportation\"\n          }\n        }\n      }\n    },\n    \"Imported Settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Paramètres importés\"\n          }\n        }\n      }\n    },\n    \"In Browser\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dans le navigateur\"\n          }\n        }\n      }\n    },\n    \"In Default Browser\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dans le navigateur par défaut\"\n          }\n        }\n      }\n    },\n    \"In Mlem\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dans Mlem\"\n          }\n        }\n      }\n    },\n    \"In Reader\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dans le « mode lecture »\"\n          }\n        }\n      }\n    },\n    \"In the Fediverse, many different links can point to the same piece of content. Choose which site to use when sharing content.\" : {\n\n    },\n    \"Inbox\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Notifications\"\n          }\n        }\n      }\n    },\n    \"Inbox is outdated\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La boîte de réception est périmée\"\n          }\n        }\n      }\n    },\n    \"Incidents\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Incidents\"\n          }\n        }\n      }\n    },\n    \"Indicate link thumbnails with an icon.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Indiquer les miniatures des liens avec une icône.\"\n          }\n        }\n      }\n    },\n    \"Infinite Scroll\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Défilement infini\"\n          }\n        }\n      }\n    },\n    \"Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Instance\"\n          }\n        }\n      }\n    },\n    \"Instance is private\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"L'instance est privée\"\n          }\n        }\n      }\n    },\n    \"Instance Link\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lien de l'instance\"\n          }\n        }\n      }\n    },\n    \"Instances\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Instances\"\n          }\n        }\n      }\n    },\n    \"Interaction Bar\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Barre d’intéraction\"\n          }\n        }\n      }\n    },\n    \"It's turtles all the way down\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ce sont des tortues tout le long\"\n          }\n        }\n      }\n    },\n    \"Italic\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Italique\"\n          }\n        }\n      }\n    },\n    \"john_doe\" : {\n      \"comment\" : \"Translate this into a similar placeholder name in your language.\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"jean_dupont\"\n          }\n        }\n      }\n    },\n    \"john_doe@example.com\" : {\n      \"comment\" : \"Translate \\\"john_doe\\\" into the equivalent placeholder name in your language, and \\\"example.com\\\" into a suitable example domain for your locale.\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"jean_dupont@exemple.com\"\n          }\n        }\n      }\n    },\n    \"john@example.com\" : {\n      \"comment\" : \"Translate \\\"john\\\" into the equivalent placeholder name in your language, and \\\"example.com\\\" into a suitable example domain for your locale. The placeholder name should be as short as possible, as this string is displayed in contexts where there may not be much space horizontally.\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"jean@exemple.com\"\n          }\n        }\n      }\n    },\n    \"Join %@ active users on Lemmy.world\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rejoindre %@ utilisateurs actifs sur Lemmy.world\"\n          }\n        }\n      }\n    },\n    \"Join Discord Server\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rejoindre le serveur Discord\"\n          }\n        }\n      }\n    },\n    \"Join Matrix Room\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rejoindre l’espace Matrix\"\n          }\n        }\n      }\n    },\n    \"Jump Button\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bouton de saut\"\n          }\n        }\n      }\n    },\n    \"Just Now\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Maintenant\"\n          }\n        }\n      }\n    },\n    \"Keep\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Conserver\"\n          }\n        }\n      }\n    },\n    \"Keep Place\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Conserver l’Emplacement\"\n          }\n        }\n      }\n    },\n    \"Key\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Clé\"\n          }\n        }\n      }\n    },\n    \"Keywords\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mots-clés\"\n          }\n        }\n      }\n    },\n    \"Large\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Large\"\n          }\n        }\n      }\n    },\n    \"Last %lld days\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ces %lld derniers jours\"\n          }\n        }\n      }\n    },\n    \"Learn more...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"En savoir davantage…\"\n          }\n        }\n      }\n    },\n    \"Left\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Gauche\"\n          }\n        }\n      }\n    },\n    \"Lemmy\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lemmy\"\n          }\n        }\n      }\n    },\n    \"Lemmy Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Communauté Lemmy\"\n          }\n        }\n      }\n    },\n    \"Let's Go\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"C’est parti\"\n          }\n        }\n      }\n    },\n    \"Licenses\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Licences\"\n          }\n        }\n      }\n    },\n    \"Link\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lien\"\n          }\n        }\n      }\n    },\n    \"Literals\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Littéraux\"\n          }\n        }\n      }\n    },\n    \"Load directly from %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Charger directement depuis %@\"\n          }\n        }\n      }\n    },\n    \"Load More\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Charger davantage\"\n          }\n        }\n      }\n    },\n    \"Load This Image Directly\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Charger cette image directement\"\n          }\n        }\n      }\n    },\n    \"Loading instance details\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Chargement des détails de l’instance\"\n          }\n        }\n      }\n    },\n    \"Loading...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Chargement…\"\n          }\n        }\n      }\n    },\n    \"Local\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Local\"\n          }\n        }\n      }\n    },\n    \"Local Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Local uniquement\"\n          }\n        }\n      }\n    },\n    \"Local Options\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Options locales\"\n          }\n        }\n      }\n    },\n    \"local.subscriber.count.text\" : {\n      \"comment\" : \"Used in the \\\"Details\\\" tab of a community page to indicate how many local subscribers use the instance. E.g. \\\"56 on lemmy.world\\\".\",\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"%1$lld on %2$@\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$lld dans %2$@\"\n          }\n        }\n      }\n    },\n    \"Location\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Localisation\"\n          }\n        }\n      }\n    },\n    \"Lock\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Verrouillage\"\n          }\n        }\n      }\n    },\n    \"Lock Post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Verrouiller la publication\"\n          }\n        }\n      }\n    },\n    \"Log In\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Se connecter\"\n          }\n        }\n      }\n    },\n    \"Log in or sign up to view your inbox.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Connectez-vous ou inscrivez-vous pour voir votre boîte de réception.\"\n          }\n        }\n      }\n    },\n    \"Log in or sign up to view your subscriptions.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Connectez-vous ou inscrivez-vous pour consulter vos abonnements.\"\n          }\n        }\n      }\n    },\n    \"Long Press Action\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Action d’appui long\"\n          }\n        }\n      }\n    },\n    \"Looking for something? Read posts are hidden.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous cherchez quelque chose ? Les publications lues sont masquées.\"\n          }\n        }\n      }\n    },\n    \"Low\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bas\"\n          }\n        }\n      }\n    },\n    \"Lowest\" : {\n\n    },\n    \"Manage how Mlem handles links and control how images and videos are displayed.\" : {\n\n    },\n    \"Manage how Mlem interacts with Lemmy instances and other websites.\" : {\n\n    },\n    \"Manage settings related to content moderation.\" : {\n\n    },\n    \"Manage your overall setup for Mlem.\" : {\n\n    },\n    \"Mark Read\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Marquer comme lu\"\n          }\n        }\n      }\n    },\n    \"Mark Read on Scroll\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Marquer comme lu au défilement\"\n          }\n        }\n      }\n    },\n    \"Mark Unread\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Marquer comme non lu\"\n          }\n        }\n      }\n    },\n    \"Matrix Room\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Espace Matrix\"\n          }\n        }\n      }\n    },\n    \"Maximum Comment Depth\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Profondeur maximale des commentaires\"\n          }\n        }\n      }\n    },\n    \"Maximum Depth\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Profondeur maximale\"\n          }\n        }\n      }\n    },\n    \"Media & Links\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Médias et liens\"\n          }\n        }\n      }\n    },\n    \"Medium\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Moyen\"\n          }\n        }\n      }\n    },\n    \"Mentions\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mentions\"\n          }\n        }\n      }\n    },\n    \"Mentions Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Uniquement les mentions\"\n          }\n        }\n      }\n    },\n    \"Message Reports\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rapports de messages\"\n          }\n        }\n      }\n    },\n    \"Message Reports Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rapports de messages uniquement\"\n          }\n        }\n      }\n    },\n    \"Message was deleted\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le message a été supprimé\"\n          }\n        }\n      }\n    },\n    \"Messages\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Messages\"\n          }\n        }\n      }\n    },\n    \"Messages Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Messages uniquement\"\n          }\n        }\n      }\n    },\n    \"Mlem Developer\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Développeur de Mlem\"\n          }\n        }\n      }\n    },\n    \"Mlem Privacy Policy\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Politique de confidentialité de Mlem\"\n          }\n        }\n      }\n    },\n    \"Mlem uses a Google API to fetch website icon URLs. If you'd prefer not to use this, you can choose to hide website icons.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mlem utilise une API Google pour récupérer les URL des icônes de sites web. Si vous préférez ne pas l'utiliser, vous pouvez choisir de masquer les icônes de sites web.\"\n          }\n        }\n      }\n    },\n    \"Mlem will always try to load from the proxy first.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mlem essaiera toujours de charger à partir du proxy en premier.\"\n          }\n        }\n      }\n    },\n    \"Mod Mail\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Messages de modération\"\n          }\n        }\n      }\n    },\n    \"Mod Mail Action Layouts\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Disposition des actions Mod Mail\"\n          }\n        }\n      }\n    },\n    \"Mod Mail Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Uniquement les messages de modération\"\n          }\n        }\n      }\n    },\n    \"Moderated\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Modéré\"\n          }\n        }\n      }\n    },\n    \"Moderation\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Modération\"\n          }\n        }\n      }\n    },\n    \"Moderation...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Modération...\"\n          }\n        }\n      }\n    },\n    \"Moderator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Modérateur\"\n          }\n        }\n      }\n    },\n    \"Moderator Actions\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Actions du modérateur\"\n          }\n        }\n      }\n    },\n    \"Moderator was appointed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le modérateur a été nommé\"\n          }\n        }\n      }\n    },\n    \"Moderator was removed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le Modérateur a été retiré\"\n          }\n        }\n      }\n    },\n    \"Modlog\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Log de modération\"\n          }\n        }\n      }\n    },\n    \"Modlogs\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Logs de modération\"\n          }\n        }\n      }\n    },\n    \"Monochrome\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Monochrome\"\n          }\n        }\n      }\n    },\n    \"More\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Plus\"\n          }\n        }\n      }\n    },\n    \"More Info\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Plus d'info\"\n          }\n        }\n      }\n    },\n    \"More Replies\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Plus de réponses\"\n          }\n        }\n      }\n    },\n    \"More Widgets...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Plus de widgets...\"\n          }\n        }\n      }\n    },\n    \"More...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Plus...\"\n          }\n        }\n      }\n    },\n    \"Most Comments\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Plus de commentaires\"\n          }\n        }\n      }\n    },\n    \"Most Recent\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le plus récent\"\n          }\n        }\n      }\n    },\n    \"Mozilla Observatory\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mozilla Observatory\"\n          }\n        }\n      }\n    },\n    \"Multiple Columns\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Colonnes multiples\"\n          }\n        }\n      }\n    },\n    \"Mute\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mettre en sourdine\"\n          }\n        }\n      }\n    },\n    \"Mute Videos\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vidéos en sourdine\"\n          }\n        }\n      }\n    },\n    \"My Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mon instance\"\n          }\n        }\n      }\n    },\n    \"My Profile\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mon profil\"\n          }\n        }\n      }\n    },\n    \"Name\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nom\"\n          }\n        }\n      }\n    },\n    \"Never\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Jamais\"\n          }\n        }\n      }\n    },\n    \"New\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nouveau\"\n          }\n        }\n      }\n    },\n    \"New Comments\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nouveau commentaire\"\n          }\n        }\n      }\n    },\n    \"New Keyword...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nouveau mot clé...\"\n          }\n        }\n      }\n    },\n    \"New Literal...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nouveau littéral…\"\n          }\n        }\n      }\n    },\n    \"New Password\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nouveau mot de passe\"\n          }\n        }\n      }\n    },\n    \"New password is invalid\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nouveau mot de passe n’est pas valide\"\n          }\n        }\n      }\n    },\n    \"New password must be between %lld and %lld characters long.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"New password must be between %1$lld and %2$lld characters long.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nouveau mot de passe doit comporter entre %1$lld et %2$lld caractères.\"\n          }\n        }\n      }\n    },\n    \"New Post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nouvelle publication\"\n          }\n        }\n      }\n    },\n    \"news@example.com\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"news@example.com\"\n          }\n        }\n      }\n    },\n    \"Next\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Suivant\"\n          }\n        }\n      }\n    },\n    \"Nickname\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Surnom\"\n          }\n        }\n      }\n    },\n    \"No\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Non\"\n          }\n        }\n      }\n    },\n    \"No comments found\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Aucun commentaire trouvé\"\n          }\n        }\n      }\n    },\n    \"No image\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Aucune image\"\n          }\n        }\n      }\n    },\n    \"No reason given\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Pas de motif donné\"\n          }\n        }\n      }\n    },\n    \"No URL Copied\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Pas d’URL copiée\"\n          }\n        }\n      }\n    },\n    \"Non-Text Indicators\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Indicateur non textuel\"\n          }\n        }\n      }\n    },\n    \"None\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Aucun\"\n          }\n        }\n      }\n    },\n    \"Not Guaranteed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Non garanti\"\n          }\n        }\n      }\n    },\n    \"Note\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Note\"\n          }\n        }\n      }\n    },\n    \"Notification Badge\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Badge de notification\"\n          }\n        }\n      }\n    },\n    \"Notifications will be sent to %@.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Les notifications seront envoyées vers %@.\"\n          }\n        }\n      }\n    },\n    \"Now\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Maintenant\"\n          }\n        }\n      }\n    },\n    \"NSFW\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"NSFW\"\n          }\n        }\n      }\n    },\n    \"NSFW Communities\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Communautés NSFW\"\n          }\n        }\n      }\n    },\n    \"NSFW Content\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Contenu NSFW\"\n          }\n        }\n      }\n    },\n    \"NSFW Tag\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Étiquette NSFW\"\n          }\n        }\n      }\n    },\n    \"Ocean\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Océan\"\n          }\n        }\n      }\n    },\n    \"Off\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Off\"\n          }\n        }\n      }\n    },\n    \"Old\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vieux\"\n          }\n        }\n      }\n    },\n    \"Older\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Plus vieux\"\n          }\n        }\n      }\n    },\n    \"OLED\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"OLED\"\n          }\n        }\n      }\n    },\n    \"On\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"On\"\n          }\n        }\n      }\n    },\n    \"Once approved, you'll be able to log in to your account from the Settings tab.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Une fois approuvé, vous pourrez vous connecter à votre compte depuis l'onglet des paramètres.\"\n          }\n        }\n      }\n    },\n    \"One of more of your posts failed to send.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Un ou plusieurs de vos messages n'ont pas pu être envoyés.\"\n          }\n        }\n      }\n    },\n    \"Only in Profile\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Uniquement dans le profil\"\n          }\n        }\n      }\n    },\n    \"Open\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ouvrir\"\n          }\n        }\n      }\n    },\n    \"Open Account Switcher\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ouvrir le sélectionneur de comptes\"\n          }\n        }\n      }\n    },\n    \"Open Authenticator App...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ouvrir l’app d’authentification...\"\n          }\n        }\n      }\n    },\n    \"Open External Links\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ouvrir les liens externes\"\n          }\n        }\n      }\n    },\n    \"Open in Browser\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ouvrir dans le navigateur\"\n          }\n        }\n      }\n    },\n    \"Open in Reader\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ouvrir en « mode lecture »\"\n          }\n        }\n      }\n    },\n    \"Open Mail App\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ouvrir l’application de mails\"\n          }\n        }\n      }\n    },\n    \"Open URL from Clipboard\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copier l’URL dans le Presse-papiers\"\n          }\n        }\n      }\n    },\n    \"Optional Description\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Description optionnelle\"\n          }\n        }\n      }\n    },\n    \"Orange\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Orange\"\n          }\n        }\n      }\n    },\n    \"Original Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Instance originale\"\n          }\n        }\n      }\n    },\n    \"Original Poster\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Publieur originel\"\n          }\n        }\n      }\n    },\n    \"Other\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Autre\"\n          }\n        }\n      }\n    },\n    \"Outline\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Contour\"\n          }\n        }\n      }\n    },\n    \"Outline Thickness\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Épaisseur de contour\"\n          }\n        }\n      }\n    },\n    \"Outside NSFW Communities\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"En dehors de Communautés NSFW\"\n          }\n        }\n      }\n    },\n    \"Overview\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Aperçu\"\n          }\n        }\n      }\n    },\n    \"Parent Comments\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Commentaires parents\"\n          }\n        }\n      }\n    },\n    \"Password\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mot de passe\"\n          }\n        }\n      }\n    },\n    \"Password must be %lld characters or more.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le mot de passe doit contenir %lld caractères ou plus.\"\n          }\n        }\n      }\n    },\n    \"Passwords don't match.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Les mots de passe en correspondent pas.\"\n          }\n        }\n      }\n    },\n    \"Paste\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Coller\"\n          }\n        }\n      }\n    },\n    \"Pause\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Pause\"\n          }\n        }\n      }\n    },\n    \"Permanent\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Définitivement\"\n          }\n        }\n      }\n    },\n    \"Permanently delete %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer définitivement %@\"\n          }\n        }\n      }\n    },\n    \"Personal Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Perspnnel Seulement\"\n          }\n        }\n      }\n    },\n    \"Photo Library\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Gallerie de photos\"\n          }\n        }\n      }\n    },\n    \"Photos\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Photos\"\n          }\n        }\n      }\n    },\n    \"PieFed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"PieFed\"\n          }\n        }\n      }\n    },\n    \"Pin\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Épingler\"\n          }\n        }\n      }\n    },\n    \"Pin Post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Épingler la Publication\"\n          }\n        }\n      }\n    },\n    \"Pin to community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Épingler dans la communauté\"\n          }\n        }\n      }\n    },\n    \"Pin to Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Épingler la Communauté\"\n          }\n        }\n      }\n    },\n    \"Pin to Community or Instance?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Épingler dans la communauté ou l’instance ?\"\n          }\n        }\n      }\n    },\n    \"Pin to instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Épingler sur l'instance\"\n          }\n        }\n      }\n    },\n    \"Pin to Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Épingler l’Instance\"\n          }\n        }\n      }\n    },\n    \"Pin...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Épingler…\"\n          }\n        }\n      }\n    },\n    \"Pink\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rose\"\n          }\n        }\n      }\n    },\n    \"Play\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lecture\"\n          }\n        }\n      }\n    },\n    \"Poll has ended\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le sondage est terminé\"\n          }\n        }\n      }\n    },\n    \"Popular\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Populaire\"\n          }\n        }\n      }\n    },\n    \"Post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Publication\"\n          }\n        }\n      }\n    },\n    \"Post Downvotes\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votes négatifs sur les publications\"\n          }\n        }\n      }\n    },\n    \"Post failed to send.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La publication n’a pas pu être envoyée.\"\n          }\n        }\n      }\n    },\n    \"Post Read Indicator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Indicateur de lecture de publication\"\n          }\n        }\n      }\n    },\n    \"Post Reports\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rapports de publication\"\n          }\n        }\n      }\n    },\n    \"Post Reports Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rapports de publication seulement\"\n          }\n        }\n      }\n    },\n    \"Post Size\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Taille de la publication\"\n          }\n        }\n      }\n    },\n    \"Post Upvotes\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votes positifs sur les publications\"\n          }\n        }\n      }\n    },\n    \"Post was locked\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La publication a été verrouillée\"\n          }\n        }\n      }\n    },\n    \"Post was pinned to %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La publication a été épinglée sur %@\"\n          }\n        }\n      }\n    },\n    \"Post was purged\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La publication a été purgée\"\n          }\n        }\n      }\n    },\n    \"Post was removed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La publication a été retirée\"\n          }\n        }\n      }\n    },\n    \"Post was restored\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La publication a été restaurée\"\n          }\n        }\n      }\n    },\n    \"Post was unlocked\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La publication a été déverrouillée\"\n          }\n        }\n      }\n    },\n    \"Post was unpinned from %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La publication a été désépinglée depuis %@\"\n          }\n        }\n      }\n    },\n    \"Posts\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Publications\"\n          }\n        }\n      }\n    },\n    \"Posts from %@ communities\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Publications des communautés %@\"\n          }\n        }\n      }\n    },\n    \"Posts from all federated instances\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Publications de toutes les instances fédérées\"\n          }\n        }\n      }\n    },\n    \"Posts from communities you moderate\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Publications depuis les communautés que vous modérez\"\n          }\n        }\n      }\n    },\n    \"Posts from communities you subscribe to\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Publications depuis les communautés suivies\"\n          }\n        }\n      }\n    },\n    \"Posts from popular communities\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Publications des communautés populaires\"\n          }\n        }\n      }\n    },\n    \"Pride\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Fierté\"\n          }\n        }\n      }\n    },\n    \"Privacy\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vie privée\"\n          }\n        }\n      }\n    },\n    \"Privacy Policy\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Politique de Confidentialité\"\n          }\n        }\n      }\n    },\n    \"Private\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Privé\"\n          }\n        }\n      }\n    },\n    \"Profile\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Profil\"\n          }\n        }\n      }\n    },\n    \"Profile Tab Label\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Étiquette de l'onglet de profil\"\n          }\n        }\n      }\n    },\n    \"Proxy Failure\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Échec du proxy\"\n          }\n        }\n      }\n    },\n    \"Purge\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Purger\"\n          }\n        }\n      }\n    },\n    \"Purge Comment\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Purger un commentaire\"\n          }\n        }\n      }\n    },\n    \"Purge Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Purger une communauté\"\n          }\n        }\n      }\n    },\n    \"Purge Person\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Purger une personne\"\n          }\n        }\n      }\n    },\n    \"Purge Post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Purger une publication\"\n          }\n        }\n      }\n    },\n    \"Purge User\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Purger un utilisateur\"\n          }\n        }\n      }\n    },\n    \"Purged content is erased from the database and cannot be restored.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le contenu purgé est effacé de la base de données et ne peut pas être restauré.\"\n          }\n        }\n      }\n    },\n    \"Quick Look\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Aperçu rapide\"\n          }\n        }\n      }\n    },\n    \"Quote\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Citation\"\n          }\n        }\n      }\n    },\n    \"Ranks posts based on the post score and creation time.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ranks posts based on the post score and creation time.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Classe les publications en fonction du score de publication et du temps de création.\"\n          }\n        }\n      }\n    },\n    \"Ranks posts based on the post score and the time since the last comment was created.\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ranks posts based on the post score and the time since the last comment was created.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Classe les publications en se basant sur son score et la durée écoulée depuis la création du dernier commentaire.\"\n          }\n        }\n      }\n    },\n    \"Re-Export\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Réexporter\"\n          }\n        }\n      }\n    },\n    \"Read %lld posts\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lu %lld publications\"\n          }\n        }\n      }\n    },\n    \"Read Indicator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Indicateur de lecture\"\n          }\n        }\n      }\n    },\n    \"Read posts are shown with dimmed title text. If you like, you can choose an additional way of indicating read status.\" : {\n\n    },\n    \"Really add NSFW tag?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vraiment ajouter le tag NSFW ?\"\n          }\n        }\n      }\n    },\n    \"Really apply this configuration to all interaction bars?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Faut-il vraiment appliquer cette configuration à toutes les barres d’interaction ?\"\n          }\n        }\n      }\n    },\n    \"Really apply this configuration to all other content types?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Faut-il vraiment appliquer cette configuration à tous les autres types de contenu ?\"\n          }\n        }\n      }\n    },\n    \"Really appoint %@ as a moderator of %@?\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Really appoint %1$@ as a moderator of %2$@?\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment nommer %1$@ comme modérateur de %2$@ ?\"\n          }\n        }\n      }\n    },\n    \"Really appoint %@ as an administrator of %@?\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Really appoint %1$@ as an administrator of %2$@?\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment nommer %1$@ comme administrateur de %2$@ ?\"\n          }\n        }\n      }\n    },\n    \"Really appoint this user as a moderator of %@?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment nommer cet utilisateur comme modérateur de %@ ?\"\n          }\n        }\n      }\n    },\n    \"Really appoint this user as an administrator of %@?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment nommer cet utilisateur comme administrateur de %@ ?\"\n          }\n        }\n      }\n    },\n    \"Really block this community?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment bloquer cette communauté ?\"\n          }\n        }\n      }\n    },\n    \"Really block this instance?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment bloquer cette instance ?\"\n          }\n        }\n      }\n    },\n    \"Really block this user?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment bloquer cet utilisateur ?\"\n          }\n        }\n      }\n    },\n    \"Really delete %@?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment supprimer %@ ?\"\n          }\n        }\n      }\n    },\n    \"Really delete?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vraiment supprimer ?\"\n          }\n        }\n      }\n    },\n    \"Really lock this post?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vraiment verrouiller cette publication ?\"\n          }\n        }\n      }\n    },\n    \"Really pin this post to %@?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment épingler ce message sur %@ ?\"\n          }\n        }\n      }\n    },\n    \"Really pin this post to the community?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment épingler ce message dans la communauté ?\"\n          }\n        }\n      }\n    },\n    \"Really pin this post?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vraiment épingler cette publication ?\"\n          }\n        }\n      }\n    },\n    \"Really remove %@?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment supprimer %@ ?\"\n          }\n        }\n      }\n    },\n    \"Really remove administrator %@ from %@?\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Really remove administrator %1$@ from %2$@?\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment supprimer l’administrateur %1$@ de %2$@ ?\"\n          }\n        }\n      }\n    },\n    \"Really remove moderator %@ from %@?\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Really remove moderator %1$@ from %2$@?\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment supprimer le modérateur %1$@ de %2$@ ?\"\n          }\n        }\n      }\n    },\n    \"Really remove NSFW tag?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vraiment retirer le tag NSFW ?\"\n          }\n        }\n      }\n    },\n    \"Really sign out of %@?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment vous déconnecter de %@ ?\"\n          }\n        }\n      }\n    },\n    \"Really unlock this post?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment débloquer ce post ?\"\n          }\n        }\n      }\n    },\n    \"Really unpin this post from %@?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment retirer ce message de %@ ?\"\n          }\n        }\n      }\n    },\n    \"Really unpin this post from the community?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voulez-vous vraiment retirer ce message de la communauté ?\"\n          }\n        }\n      }\n    },\n    \"Really unpin this post?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vraiment désépingler cette publication ?\"\n          }\n        }\n      }\n    },\n    \"Reason\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Motif\"\n          }\n        }\n      }\n    },\n    \"Reason (Optional)\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Motif (optionnel)\"\n          }\n        }\n      }\n    },\n    \"Reason:\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Motif :\"\n          }\n        }\n      }\n    },\n    \"Received %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Reçu %@\"\n          }\n        }\n      }\n    },\n    \"Recent Checks\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vérifications Récentes\"\n          }\n        }\n      }\n    },\n    \"Recently Searched\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Recherches Récentes\"\n          }\n        }\n      }\n    },\n    \"Redo\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Refaire\"\n          }\n        }\n      }\n    },\n    \"Reduce Motion\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Réduire les animations\"\n          }\n        }\n      }\n    },\n    \"Refresh\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rafraîchir\"\n          }\n        }\n      }\n    },\n    \"Refresh Token\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Token de rafraîchissement\"\n          }\n        }\n      }\n    },\n    \"Registration\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Inscription\"\n          }\n        }\n      }\n    },\n    \"Registration Applications\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Demandes d’inscription\"\n          }\n        }\n      }\n    },\n    \"Registrations are closed on this instance.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Les inscriptions sont closes sur cette instance.\"\n          }\n        }\n      }\n    },\n    \"Reload\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rechargement\"\n          }\n        }\n      }\n    },\n    \"Reload on Switch\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Recharger au changement\"\n          }\n        }\n      }\n    },\n    \"Remember Search History\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Se souvenir de l’historique de recherche\"\n          }\n        }\n      }\n    },\n    \"Remove\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer\"\n          }\n        }\n      }\n    },\n    \"Remove Administrator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer un administrateur\"\n          }\n        }\n      }\n    },\n    \"Remove Comment\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer un commentaire\"\n          }\n        }\n      }\n    },\n    \"Remove Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer une communauté\"\n          }\n        }\n      }\n    },\n    \"Remove Content\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer un contenu\"\n          }\n        }\n      }\n    },\n    \"Remove Moderator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer un modérateur\"\n          }\n        }\n      }\n    },\n    \"Remove NSFW Tag\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Retirer le tag NSFW\"\n          }\n        }\n      }\n    },\n    \"Remove Post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer une publication\"\n          }\n        }\n      }\n    },\n    \"Remove Recent Search\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimer une recherche récente\"\n          }\n        }\n      }\n    },\n    \"Removed: %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimé : %@\"\n          }\n        }\n      }\n    },\n    \"Removed: %@\\nFrom: %@\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Removed: %1$@\\nFrom: %2$@\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Supprimé : %1$@\\nDe : %2$@\"\n          }\n        }\n      }\n    },\n    \"Replies\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Réponses\"\n          }\n        }\n      }\n    },\n    \"Replies Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Réponses uniquement\"\n          }\n        }\n      }\n    },\n    \"Replies, mentions and messages\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Réponses, mentions et messages\"\n          }\n        }\n      }\n    },\n    \"Reply\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Réponse\"\n          }\n        }\n      }\n    },\n    \"Reply Counter\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Compteur de réponse\"\n          }\n        }\n      }\n    },\n    \"Report\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Signaler\"\n          }\n        }\n      }\n    },\n    \"Reported %@ by %@\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Reported %1$@ by %2$@\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Signalé %1$@ par %2$@\"\n          }\n        }\n      }\n    },\n    \"Reports\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Signalements\"\n          }\n        }\n      }\n    },\n    \"Reports and Registration Applications\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Signalements et demandes d’inscriptions\"\n          }\n        }\n      }\n    },\n    \"Reports Email Admins\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Signalements d’emails d’admins\"\n          }\n        }\n      }\n    },\n    \"Reports from communities you moderate\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Signalements depuis des communautés que vous modérez\"\n          }\n        }\n      }\n    },\n    \"Reports Only\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Signalements uniquement\"\n          }\n        }\n      }\n    },\n    \"Requires Application\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nécessite une inscription\"\n          }\n        }\n      }\n    },\n    \"Reset\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Réinitialsier\"\n          }\n        }\n      }\n    },\n    \"Resolve\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Résoudre\"\n          }\n        }\n      }\n    },\n    \"Resolved\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Résolu\"\n          }\n        }\n      }\n    },\n    \"Resolved by %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Résolu par %@\"\n          }\n        }\n      }\n    },\n    \"Resolving...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Résolution…\"\n          }\n        }\n      }\n    },\n    \"Response Time\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Temps de réponse\"\n          }\n        }\n      }\n    },\n    \"Restore\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Restaurer\"\n          }\n        }\n      }\n    },\n    \"Restore Settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Paramètres de restauration\"\n          }\n        }\n      }\n    },\n    \"Restored\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Restauré\"\n          }\n        }\n      }\n    },\n    \"Restored Settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Paramètres restaurés\"\n          }\n        }\n      }\n    },\n    \"Right\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Droite\"\n          }\n        }\n      }\n    },\n    \"Row Size\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Taille de ligne\"\n          }\n        }\n      }\n    },\n    \"Safety & Filtering\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sécurité et filtrage\"\n          }\n        }\n      }\n    },\n    \"Save\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Enregistrer\"\n          }\n        }\n      }\n    },\n    \"Save and Restore\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Enregistrer et restaurer\"\n          }\n        }\n      }\n    },\n    \"Save Settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Enregistrer les réglages\"\n          }\n        }\n      }\n    },\n    \"Save the current settings and restore them later.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Enregistrez les réglages actuels et restaurez-les ultérieurement.\"\n          }\n        }\n      }\n    },\n    \"Saved\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Enregistré\"\n          }\n        }\n      }\n    },\n    \"Saved Settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Paramètres enregistrés\"\n          }\n        }\n      }\n    },\n    \"Scaled\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mis à l’échelle\"\n          }\n        }\n      }\n    },\n    \"Score\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Score\"\n          }\n        }\n      }\n    },\n    \"Score Counter\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Compteur de score\"\n          }\n        }\n      }\n    },\n    \"Search\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Recherche\"\n          }\n        }\n      }\n    },\n    \"Search for comments\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rechercher des commentaires\"\n          }\n        }\n      }\n    },\n    \"Search for communities\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rechercher des communautés\"\n          }\n        }\n      }\n    },\n    \"Search for Lemmy instances\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rechercher des instances Lemmy\"\n          }\n        }\n      }\n    },\n    \"Search for posts\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rechercher des publications\"\n          }\n        }\n      }\n    },\n    \"Search for users\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rechercher des utilisateurs\"\n          }\n        }\n      }\n    },\n    \"Search...\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Search…\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Recherche…\"\n          }\n        }\n      }\n    },\n    \"See All\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voir tout\"\n          }\n        }\n      }\n    },\n    \"Select Text\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sélectionner le texte\"\n          }\n        }\n      }\n    },\n    \"Send\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Envoyer\"\n          }\n        }\n      }\n    },\n    \"Send a Message...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Envoyer un message...\"\n          }\n        }\n      }\n    },\n    \"Send Message\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Envoyer un message\"\n          }\n        }\n      }\n    },\n    \"Send Notifications to Email\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Envoyer des notifications par e-mail\"\n          }\n        }\n      }\n    },\n    \"Send to Lemmy User\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Envoyer à un utilisateur Lemmy\"\n          }\n        }\n      }\n    },\n    \"Sent %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Envoyé %@\"\n          }\n        }\n      }\n    },\n    \"Separate Actions Using\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Séparer les actions à l'aide de\"\n          }\n        }\n      }\n    },\n    \"Separate Menu\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Menu séparé\"\n          }\n        }\n      }\n    },\n    \"Settings\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Paramètres\"\n          }\n        }\n      }\n    },\n    \"Settings Icons\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Paramètres des icônes\"\n          }\n        }\n      }\n    },\n    \"Share\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Partager\"\n          }\n        }\n      }\n    },\n    \"Share Links\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Partager les liens\"\n          }\n        }\n      }\n    },\n    \"Share links using %@. When someone opens the link, they can choose which instance to use.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Partager des liens avec %@. Lorsqu'un utilisateur ouvre le lien, il peut choisir l'instance à utiliser.\"\n          }\n        }\n      }\n    },\n    \"Share links using the instance that the content originated from.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Partager des liens en utilisant l’instance d’où provient le contenu.\"\n          }\n        }\n      }\n    },\n    \"Share links using the instance you are currently connected to.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Partager des liens en utilisant l'instance à laquelle vous êtes actuellement connectée.\"\n          }\n        }\n      }\n    },\n    \"Share using...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Partager via…\"\n          }\n        }\n      }\n    },\n    \"Share...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Partager...\"\n          }\n        }\n      }\n    },\n    \"Show\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher\"\n          }\n        }\n      }\n    },\n    \"Show Account Age\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher l’âge du compte\"\n          }\n        }\n      }\n    },\n    \"Show All Actions in Feed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher toutes les actions dans le flux\"\n          }\n        }\n      }\n    },\n    \"Show Avatar\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher l’avatar\"\n          }\n        }\n      }\n    },\n    \"Show Bot Accounts\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher les comptes robots\"\n          }\n        }\n      }\n    },\n    \"Show content flagged as Not Safe For Work.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher les contenus étiquetés comme « Not Safe For Work ».\"\n          }\n        }\n      }\n    },\n    \"Show Controls\" : {\n\n    },\n    \"Show Details\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher les détails\"\n          }\n        }\n      }\n    },\n    \"Show Downvotes Separately\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher séparément les votes négatifs\"\n          }\n        }\n      }\n    },\n    \"Show Events\" : {\n\n    },\n    \"Show Full URL\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher toute l’URL\"\n          }\n        }\n      }\n    },\n    \"Show Mod Names in Modlog\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher les noms de modérateurs dans le journal de modération\"\n          }\n        }\n      }\n    },\n    \"Show NSFW Content\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher le contenu NSFW\"\n          }\n        }\n      }\n    },\n    \"Show Older Incidents\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher les incidents plus vieux\"\n          }\n        }\n      }\n    },\n    \"Show Parent\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher le parent\"\n          }\n        }\n      }\n    },\n    \"Show Post\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher la publication\"\n          }\n        }\n      }\n    },\n    \"Show Read\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher les lus\"\n          }\n        }\n      }\n    },\n    \"Show Response Times\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher les temps de réponse\"\n          }\n        }\n      }\n    },\n    \"Show Results\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher les résultats\"\n          }\n        }\n      }\n    },\n    \"Show Sidebar on App Launch\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher la barre latérale au lancement de l'application\"\n          }\n        }\n      }\n    },\n    \"Show warnings when opening...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Afficher les avertissements lors de l'ouverture...\"\n          }\n        }\n      }\n    },\n    \"Showing Read\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lus affichés\"\n          }\n        }\n      }\n    },\n    \"Shown\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Affiché\"\n          }\n        }\n      }\n    },\n    \"Sign In\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Se connecter\"\n          }\n        }\n      }\n    },\n    \"Sign In to Lemmy\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Se connecter à Lemmy\"\n          }\n        }\n      }\n    },\n    \"Sign Out\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sign Out\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Se déconnecter\"\n          }\n        }\n      }\n    },\n    \"Sign Up\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"S’inscrire\"\n          }\n        }\n      }\n    },\n    \"Sign-In & Security\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Connexion et sécurité\"\n          }\n        }\n      }\n    },\n    \"Silver\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Argent\"\n          }\n        }\n      }\n    },\n    \"Similar to Hot, but ranks posts from smaller communities higher.\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Similar to \\\\\\\"Hot\\\\\\\", but ranks posts from smaller communities higher.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Similaire à « Chaud », mais met davantage en avant les publications des plus petites communautés.\"\n          }\n        }\n      }\n    },\n    \"Size\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Taille\"\n          }\n        }\n      }\n    },\n    \"Slide to Zoom\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Glisser pour zoomer\"\n          }\n        }\n      }\n    },\n    \"Slide to Zoom Images\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Glisser pour zoomer sur les images\"\n          }\n        }\n      }\n    },\n    \"Slow\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lent\"\n          }\n        }\n      }\n    },\n    \"Slur Filter\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Filtre de liaison\"\n          }\n        }\n      }\n    },\n    \"Solarized\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Solarised\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Solarisé\"\n          }\n        }\n      }\n    },\n    \"Some\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Quelques\"\n          }\n        }\n      }\n    },\n    \"Some instances proxy images to protect your privacy. In certain cases, this causes image loading to fail. You can bypass the image proxy and load directly, but this will expose your IP address to the image host.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Certaines instances utilisent des proxy d’images pour protéger votre vie privée. Dans certains cas, cela entraîne l'échec du chargement de l'image. Vous pouvez contourner le proxy d'image et charger directement, mais cela exposera votre adresse IP à l'hôte de l'image.\"\n          }\n        }\n      }\n    },\n    \"Some users set animated media as their avatar. Control whether these avatars should play their animations.\" : {\n\n    },\n    \"Sort\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Trier\"\n          }\n        }\n      }\n    },\n    \"Sort by: %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Trier par : %@\"\n          }\n        }\n      }\n    },\n    \"Sort by...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Trier par...\"\n          }\n        }\n      }\n    },\n    \"Sorting\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Triage\"\n          }\n        }\n      }\n    },\n    \"Spam\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Spam\"\n          }\n        }\n      }\n    },\n    \"Spoiler\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Divulgâcheur\"\n          }\n        }\n      }\n    },\n    \"Start writing...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Commencer à écrire…\"\n          }\n        }\n      }\n    },\n    \"Starts %@\" : {\n\n    },\n    \"Stats\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Statistiques\"\n          }\n        }\n      }\n    },\n    \"Strikethrough\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Barré\"\n          }\n        }\n      }\n    },\n    \"Style\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Style\"\n          }\n        }\n      }\n    },\n    \"Subject\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sujet\"\n          }\n        }\n      }\n    },\n    \"Submit\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Soumettre\"\n          }\n        }\n      }\n    },\n    \"Submit Application\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Soumettre l’inscription\"\n          }\n        }\n      }\n    },\n    \"Submitted\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Soumis\"\n          }\n        }\n      }\n    },\n    \"Submitting...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Envoi...\"\n          }\n        }\n      }\n    },\n    \"Subscribe\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"S'abonner\"\n          }\n        }\n      }\n    },\n    \"Subscribed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Abonné\"\n          }\n        }\n      }\n    },\n    \"Subscribers\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Abonnés\"\n          }\n        }\n      }\n    },\n    \"Subscript\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Subscript\"\n          }\n        }\n      }\n    },\n    \"Subscription Indicator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Indicateur d’abonnement\"\n          }\n        }\n      }\n    },\n    \"Subscription List\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Liste d’abonnement\"\n          }\n        }\n      }\n    },\n    \"Suggested\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Suggérées\"\n          }\n        }\n      }\n    },\n    \"Suggested Languages\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Langues suggérées\"\n          }\n        }\n      }\n    },\n    \"Superscript\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Superscript\"\n          }\n        }\n      }\n    },\n    \"Swipe Actions\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Actions de glissement\"\n          }\n        }\n      }\n    },\n    \"Swipe Anywhere to Navigate\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Glisser n’importe où pour naviguer\"\n          }\n        }\n      }\n    },\n    \"Swiping up on the tab bar will always open the account switcher.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Glisser sur la bar d’onglets va toujours ouvrir le sélectionneur de comptes.\"\n          }\n        }\n      }\n    },\n    \"Switch Account\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Changer de Compte\"\n          }\n        }\n      }\n    },\n    \"Switch account to view your inbox.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Changer de compte pour voir votre boîte de réception.\"\n          }\n        }\n      }\n    },\n    \"Switch to Most Recent Account\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Changer vers le compte le plus récent\"\n          }\n        }\n      }\n    },\n    \"Switch to this account and...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Changer de compte et…\"\n          }\n        }\n      }\n    },\n    \"Tab Bar\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Barre d'onglets\"\n          }\n        }\n      }\n    },\n    \"Table\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Tableau\"\n          }\n        }\n      }\n    },\n    \"Tap and hold items to add, remove, or rearrange them.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Appuyez longuement sur les éléments pour les ajouter, les supprimer ou les réorganiser.\"\n          }\n        }\n      }\n    },\n    \"Tap on the Jump Button whilst viewing a comment thread to scroll to the next comment.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Appuyez sur le bouton de saut lorsque vous consultez un fil de commentaires pour faire défiler jusqu'au commentaire suivant.\"\n          }\n        }\n      }\n    },\n    \"Tap to Collapse\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Appuyez pour réduire\"\n          }\n        }\n      }\n    },\n    \"Tap to show slur filter regex.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Appuyez pour afficher l'expression régulière du filtre de liaison.\"\n          }\n        }\n      }\n    },\n    \"Tap to Undo\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Tapper pour annuler\"\n          }\n        }\n      }\n    },\n    \"Tappable Links\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Liens clickables\"\n          }\n        }\n      }\n    },\n    \"Temporary\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Temporaire\"\n          }\n        }\n      }\n    },\n    \"TestFlight updated!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"TestFlight mis à jour !\"\n          }\n        }\n      }\n    },\n    \"That's all, folks!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"C'est tout, les amis !\"\n          }\n        }\n      }\n    },\n    \"The \\\"%@\\\" sort mode is only available on some instances. On unsupported instances, the \\\"Fallback\\\" sort mode will be used instead.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le mode de tri « %@ » n’est disponible que dans certaines instances. Sur les instances non supportées, le tri « Fallback » sera appliqué à la place.\"\n          }\n        }\n      }\n    },\n    \"The %@ theme only supports %@ mode.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"The %1$@ theme only supports %2$@ mode.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le thème %1$@ ne prend en charge que le mode %2$@.\"\n          }\n        }\n      }\n    },\n    \"The Fediseer\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le Fediseer\"\n          }\n        }\n      }\n    },\n    \"The Fediseer is a service that instance administrators use to identify spam instances and express their approval or disapproval of other instances.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Fediseer est un service que les administrateurs d'instance utilisent pour identifier les instances de spam et exprimer leur approbation ou leur désapprobation d'autres instances.\"\n          }\n        }\n      }\n    },\n    \"The modlog may contain disturbing or adult material.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le modlog peut contenir du contenu dérangeant ou réservé aux adultes.\"\n          }\n        }\n      }\n    },\n    \"The most recent outage was %@, and lasted for %@.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"The most recent outage was %1$@, and lasted for %2$@.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"La panne la plus récente était %1$@ et a duré %2$@.\"\n          }\n        }\n      }\n    },\n    \"The name shown in the account switcher and tab bar.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nom affiché dans le sélecteur de compte et la barre d'onglets.\"\n          }\n        }\n      }\n    },\n    \"The name shown in the account switcher.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nom affiché dans le sélecteur de compte.\"\n          }\n        }\n      }\n    },\n    \"The name that is displayed on your profile. This is not the same as your username, which cannot be changed.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nom qui apparaît sur votre profil. Il ne correspond pas à votre nom d'utilisateur, qui ne peut pas être modifié.\"\n          }\n        }\n      }\n    },\n    \"The number of child comments that can appear in a chain before the \\\"More Replies\\\" button is shown.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nombre de commentaires enfants qui peuvent apparaître dans une chaîne avant que le bouton « Plus de réponses » ne soit affiché.\"\n          }\n        }\n      }\n    },\n    \"Theme\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Thèmes\"\n          }\n        }\n      }\n    },\n    \"There were %lld recorded incidents today.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"There was %lld recorded incidents today.\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"new\",\n                  \"value\" : \"There were %lld recorded incidents today.\"\n                }\n              }\n            }\n          }\n        },\n        \"en-GB\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"There was %lld recorded incidents today.\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"There were %lld recorded incidents today.\"\n                }\n              }\n            }\n          }\n        },\n        \"fr\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"Il y a eu %lld incident enregistré aujourd'hui.\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"Il y a eu %lld incidents enregistrés aujourd'hui.\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"There were no recorded incidents today.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Aucun incident n'a été enregistré aujourd'hui.\"\n          }\n        }\n      }\n    },\n    \"These filters were saved by an older version of Mlem, and will not be compatible with future versions. To preserve compatibility, re-export your filters.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ces filtres ont été enregistrés par une ancienne version de Mlem et ne seront pas compatibles avec les versions futures. Pour préserver la compatibilité, réexportez vos filtres.\"\n          }\n        }\n      }\n    },\n    \"These options are stored locally in Mlem and not on your Lemmy account.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ces options sont stockées localement dans Mlem et non sur votre compte Lemmy.\"\n          }\n        }\n      }\n    },\n    \"This account has %lld favorite communities.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"This account has %lld favorite community.\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"new\",\n                  \"value\" : \"This account has %lld favorite communities.\"\n                }\n              }\n            }\n          }\n        },\n        \"en-GB\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"This account has %lld favourite community.\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"This account has %lld favourite communities.\"\n                }\n              }\n            }\n          }\n        },\n        \"fr\" : {\n          \"variations\" : {\n            \"plural\" : {\n              \"one\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"Ce compte a %lld communauté favorite.\"\n                }\n              },\n              \"other\" : {\n                \"stringUnit\" : {\n                  \"state\" : \"translated\",\n                  \"value\" : \"Ce compte a %lld communautés favorites.\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"This account has no favorite communities.\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"This account has no favourite communities.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ce compte n'a pas de communautés favorites.\"\n          }\n        }\n      }\n    },\n    \"This action cannot be undone.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cette action ne peut pas être annulée.\"\n          }\n        }\n      }\n    },\n    \"This cannot be changed later.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ceci ne pourra pas être changé plus tard.\"\n          }\n        }\n      }\n    },\n    \"This community has been removed.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cette communauté a été supprimée.\"\n          }\n        }\n      }\n    },\n    \"This community likely contains graphic or explicit content.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cette communauté contient probablement du contenu graphique ou explicite.\"\n          }\n        }\n      }\n    },\n    \"This content will be loaded from **%@** instead.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ce contenu sera chargé depuis **%@** à la place.\"\n          }\n        }\n      }\n    },\n    \"This feature is not available on all instances.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cette fonctionnalité n’est pas disponible sur toutes les instances.\"\n          }\n        }\n      }\n    },\n    \"This field is optional.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ce champ est facultatif.\"\n          }\n        }\n      }\n    },\n    \"This instance is not part of the Fediseer Chain of Trust.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cette instance ne fait pas partie de la chaîne de confiance Fediseer.\"\n          }\n        }\n      }\n    },\n    \"This instance is viewed very negatively by one or more trusted instances.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cette instance est perçue très négativement par une ou plusieurs instances de confiance.\"\n          }\n        }\n      }\n    },\n    \"This is turned on by default because Differentiate Without Color is enabled in System Settings.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cette option est activée par défaut car l'option « Différencier sans couleur » est activée dans les paramètres système.\"\n          }\n        }\n      }\n    },\n    \"This page can't be displayed because Cloudflare blocked the request.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cette page ne peut pas être affichée car Cloudflare a bloqué la requête.\"\n          }\n        }\n      }\n    },\n    \"This poll is no longer accepting votes.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ce sondage n'accepte plus de votes.\"\n          }\n        }\n      }\n    },\n    \"This probably contains foul language.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cela contient probablement un langage grossier.\"\n          }\n        }\n      }\n    },\n    \"This username is taken.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ce nom d'utilisateur est pris.\"\n          }\n        }\n      }\n    },\n    \"This will clear your recent searches, which cannot be undone.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cela effacera vos recherches récentes, ce qui ne peut pas être annulé.\"\n          }\n        }\n      }\n    },\n    \"This will permanently remove it from %@, not just Mlem!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cela le supprimera définitivement de %@, pas seulement de Mlem !\"\n          }\n        }\n      }\n    },\n    \"Thumbnail\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vignette\"\n          }\n        }\n      }\n    },\n    \"Thumbnail Location\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Emplacement de la vignette\"\n          }\n        }\n      }\n    },\n    \"Tiled\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Tuile\"\n          }\n        }\n      }\n    },\n    \"Time\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Temps\"\n          }\n        }\n      }\n    },\n    \"Title\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Titre\"\n          }\n        }\n      }\n    },\n    \"To confirm, please enter your password:\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Pour confirmer, entrez svp votre mot de passe :\"\n          }\n        }\n      }\n    },\n    \"To join this instance, you need to create an application and wait to be accepted.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Pour rejoindre cette instance, vous devez faire une demande et attendre d'être accepté.\"\n          }\n        }\n      }\n    },\n    \"Today\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Aujourd’hui\"\n          }\n        }\n      }\n    },\n    \"Toggle Developer Tools\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Basculer les outils développeur\"\n          }\n        }\n      }\n    },\n    \"Too many items\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Trop d’éléments\"\n          }\n        }\n      }\n    },\n    \"Top\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Top\"\n          }\n        }\n      }\n    },\n    \"Top Communities\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Top Communautés\"\n          }\n        }\n      }\n    },\n    \"Top of...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Top…\"\n          }\n        }\n      }\n    },\n    \"Top:\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Top :\"\n          }\n        }\n      }\n    },\n    \"Top...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Top…\"\n          }\n        }\n      }\n    },\n    \"Trailing\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"À la fin\"\n          }\n        }\n      }\n    },\n    \"Transfer Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Transférer la communauté\"\n          }\n        }\n      }\n    },\n    \"Transfer Ownership\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Transférer la propriété\"\n          }\n        }\n      }\n    },\n    \"Trending Communities\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Communautés Tendances\"\n          }\n        }\n      }\n    },\n    \"Troll\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Troll\"\n          }\n        }\n      }\n    },\n    \"Trust & Safety\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Confiance et sécurité\"\n          }\n        }\n      }\n    },\n    \"Try a different Captcha...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Essayer un autre captcha…\"\n          }\n        }\n      }\n    },\n    \"Turn Off\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désactiver\"\n          }\n        }\n      }\n    },\n    \"Turn Off Events\" : {\n\n    },\n    \"Turn Off Search History\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désactiver l’historique de recherche\"\n          }\n        }\n      }\n    },\n    \"Turn off search history?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désactiver l’historique de recherche ?\"\n          }\n        }\n      }\n    },\n    \"Two-Factor Authentication\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Authentification à Double-Facteur (2FA)\"\n          }\n        }\n      }\n    },\n    \"Unavailable\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Indisponible\"\n          }\n        }\n      }\n    },\n    \"Unban\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débannier\"\n          }\n        }\n      }\n    },\n    \"Unban %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débannir %@\"\n          }\n        }\n      }\n    },\n    \"Unban from Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débannir de la communauté\"\n          }\n        }\n      }\n    },\n    \"Unban from Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débannir de l'instance\"\n          }\n        }\n      }\n    },\n    \"Unban from...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débannir de...\"\n          }\n        }\n      }\n    },\n    \"Unban User\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débannir l’utilisateur\"\n          }\n        }\n      }\n    },\n    \"Unbanned: %@\\nFrom: %@\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Unbanned: %1$@\\nFrom: %2$@\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débanni : %1$@\\nDe : %2$@\"\n          }\n        }\n      }\n    },\n    \"Unbanning from...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débanissement de…\"\n          }\n        }\n      }\n    },\n    \"Unblock\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débloquer\"\n          }\n        }\n      }\n    },\n    \"Unblock Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débloquer la communauté\"\n          }\n        }\n      }\n    },\n    \"Unblock Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débloquer l'instance\"\n          }\n        }\n      }\n    },\n    \"Unblock User\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débloquer l'utilisateur\"\n          }\n        }\n      }\n    },\n    \"Unblock...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débloquer…\"\n          }\n        }\n      }\n    },\n    \"Unblocked\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débloqué\"\n          }\n        }\n      }\n    },\n    \"Unblocking...\" : {\n      \"extractionState\" : \"stale\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Déblocage…\"\n          }\n        }\n      }\n    },\n    \"Undo\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Annuler\"\n          }\n        }\n      }\n    },\n    \"Undo Downvote\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Annuler vote négatif\"\n          }\n        }\n      }\n    },\n    \"Undo Upvote\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Annuler vote positif\"\n          }\n        }\n      }\n    },\n    \"Undone!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Annulé !\"\n          }\n        }\n      }\n    },\n    \"Unfavorite\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Unfavourite\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Enlever des favoris\"\n          }\n        }\n      }\n    },\n    \"Unfavorited\" : {\n      \"localizations\" : {\n        \"en-GB\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Unfavourited\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Enlevé des favoris\"\n          }\n        }\n      }\n    },\n    \"Unhealthy for %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mauvais pour %@\"\n          }\n        }\n      }\n    },\n    \"Universal\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Universel\"\n          }\n        }\n      }\n    },\n    \"Universal Link\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lien universel\"\n          }\n        }\n      }\n    },\n    \"Unknown\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Inconnu\"\n          }\n        }\n      }\n    },\n    \"unknown host\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"hôte inconnu\"\n          }\n        }\n      }\n    },\n    \"Unlock\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Débloquer\"\n          }\n        }\n      }\n    },\n    \"Unmute\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rétablir le son\"\n          }\n        }\n      }\n    },\n    \"Unpin\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désépingler\"\n          }\n        }\n      }\n    },\n    \"Unpin from community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désépingler de la communauté\"\n          }\n        }\n      }\n    },\n    \"Unpin From Community\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désépingler de la communauté\"\n          }\n        }\n      }\n    },\n    \"Unpin from instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désépingler de l'instance\"\n          }\n        }\n      }\n    },\n    \"Unpin From Instance\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désépingler de l’instance\"\n          }\n        }\n      }\n    },\n    \"Unresolve\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ne plus résoudre\"\n          }\n        }\n      }\n    },\n    \"Unsave\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ne plus enregistrer\"\n          }\n        }\n      }\n    },\n    \"Unsubscribe\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désinscrire\"\n          }\n        }\n      }\n    },\n    \"Unsubscribed\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Désinscrit\"\n          }\n        }\n      }\n    },\n    \"Unsupported Badge\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Badge non supporté\"\n          }\n        }\n      }\n    },\n    \"Upload\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Téléverser\"\n          }\n        }\n      }\n    },\n    \"Upload this image to %@?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Téléverser cette image sur %@ ?\"\n          }\n        }\n      }\n    },\n    \"Uploading...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Téléversement…\"\n          }\n        }\n      }\n    },\n    \"Uptime\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Temps de disponibilité\"\n          }\n        }\n      }\n    },\n    \"Uptime data fetched from %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Données de disponibilité récupérées à partir de %@\"\n          }\n        }\n      }\n    },\n    \"Upvote\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vote positif\"\n          }\n        }\n      }\n    },\n    \"Upvote Counter\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Compteur de votes positifs\"\n          }\n        }\n      }\n    },\n    \"Upvote on Save\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vote positif si enregistrement\"\n          }\n        }\n      }\n    },\n    \"Upvoted\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votées positivement\"\n          }\n        }\n      }\n    },\n    \"Use Alternate Layouts\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Utiliser des dispositions alternatives\"\n          }\n        }\n      }\n    },\n    \"User\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Utilisateur\"\n          }\n        }\n      }\n    },\n    \"User Avatar\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Avatar de l'utilisateur\"\n          }\n        }\n      }\n    },\n    \"User Link\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lien de l’utilisateur\"\n          }\n        }\n      }\n    },\n    \"User Modlog\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Journal de modération de l'utilisateur\"\n          }\n        }\n      }\n    },\n    \"User or community?\" : {\n\n    },\n    \"User was banned\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"L’utilisateur a été banni\"\n          }\n        }\n      }\n    },\n    \"User was purged\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"L’utilisateur a été purgé\"\n          }\n        }\n      }\n    },\n    \"User was unbanned\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"L’utilisateur a été débanni\"\n          }\n        }\n      }\n    },\n    \"Username\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nom d’utilisateur\"\n          }\n        }\n      }\n    },\n    \"Username can only contain lowercase letters, numbers and underscores.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nom d'utilisateur ne peut contenir que des lettres minuscules, des chiffres et des traits de soulignement.\"\n          }\n        }\n      }\n    },\n    \"Username cannot be longer than %lld characters.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nom d’utilisateur ne peut pas être plus long que %lld caractères.\"\n          }\n        }\n      }\n    },\n    \"Username cannot contain %@.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nom d’utilisateur ne peut pas contenir %@.\"\n          }\n        }\n      }\n    },\n    \"Username is invalid.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nom d’utilisateur n’est pas valide.\"\n          }\n        }\n      }\n    },\n    \"Username is taken.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nom d’utilisateur est déjà pris.\"\n          }\n        }\n      }\n    },\n    \"Username must be 3 or more characters.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nom d'utilisateur doit comporter 3 caractères ou plus.\"\n          }\n        }\n      }\n    },\n    \"Username must be at least %lld characters long.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Le nom d’utilisateur doit être d’au moins %lld caractères.\"\n          }\n        }\n      }\n    },\n    \"Users\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Utilisateurs\"\n          }\n        }\n      }\n    },\n    \"Version\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Version\"\n          }\n        }\n      }\n    },\n    \"Video Saved\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vidéo enregistrée\"\n          }\n        }\n      }\n    },\n    \"View All\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Tout voir\"\n          }\n        }\n      }\n    },\n    \"View on %@\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voir sur %@\"\n          }\n        }\n      }\n    },\n    \"View on original host\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voir sur la publication originale\"\n          }\n        }\n      }\n    },\n    \"View Votes\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Voir les votes\"\n          }\n        }\n      }\n    },\n    \"Viewing %@ as guest\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lecture %@ en tant qu’invité\"\n          }\n        }\n      }\n    },\n    \"Visit\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Visiter\"\n          }\n        }\n      }\n    },\n    \"Visit Again\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Revisiter\"\n          }\n        }\n      }\n    },\n    \"Votes\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votes\"\n          }\n        }\n      }\n    },\n    \"We sent an email to %@ to verify your email address and activate your account.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Nous avons envoyé un e-mail à %@ pour vérifier votre adresse e-mail et activer votre compte.\"\n          }\n        }\n      }\n    },\n    \"Website\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Site web\"\n          }\n        }\n      }\n    },\n    \"Website Icon\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Icône du site web\"\n          }\n        }\n      }\n    },\n    \"Website Thumbnail Indicator\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Indicateur de miniature de site Web\"\n          }\n        }\n      }\n    },\n    \"Welcome %@\" : {\n      \"comment\" : \"Example: \\\"Welcome John\\\"\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bievenue %@\"\n          }\n        }\n      }\n    },\n    \"Welcome to %@\" : {\n      \"comment\" : \"Example: \\\"Welcome to lemmy.world\\\"\",\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bienvenue sur %@\"\n          }\n        }\n      }\n    },\n    \"Welcome to Lemmy!\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bienvenue sur Lemmy !\"\n          }\n        }\n      }\n    },\n    \"What is Federation?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"C’est quoi la fédération ?\"\n          }\n        }\n      }\n    },\n    \"What's New?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Quoi de neuf ?\"\n          }\n        }\n      }\n    },\n    \"When disabled, some moderator actions will only be accessible from the post page.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lorsque cette option est désactivée, certaines actions du modérateur ne seront accessibles qu'à partir de la page de publication.\"\n          }\n        }\n      }\n    },\n    \"When enabled, Mlem will ask you to confirm your choice before uploading an image to your instance.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lorsqu'il est activé, Mlem vous demandera de confirmer votre choix avant de télécharger une image sur votre instance.\"\n          }\n        }\n      }\n    },\n    \"When I Tap\" : {\n\n    },\n    \"Would you like to open this email address in your mail app?\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Souhaitez-vous ouvrir cette adresse e-mail dans votre application de messagerie?\"\n          }\n        }\n      }\n    },\n    \"Wrap Code Block Lines\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Envelopper les lignes du bloc de code\"\n          }\n        }\n      }\n    },\n    \"Write a bit about yourself...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Écrivez un peu sur vous-même...\"\n          }\n        }\n      }\n    },\n    \"Yes\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Oui\"\n          }\n        }\n      }\n    },\n    \"You are browsing %@ as a guest. If you'd like to vote or reply, you'll need to log in or sign up.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous parcourez %@ en tant qu'invité. Si vous souhaitez voter ou répondre, vous devez vous connecter ou vous inscrire.\"\n          }\n        }\n      }\n    },\n    \"You are required to provide an email on this instance.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous devez fournir une adresse e-mail sur cette instance.\"\n          }\n        }\n      }\n    },\n    \"You can also turn off search history completely for this account.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous pouvez également désactiver complètement l’historique de recherche pour ce compte.\"\n          }\n        }\n      }\n    },\n    \"You cannot change your vote.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous ne pouvez pas modifier votre vote.\"\n          }\n        }\n      }\n    },\n    \"You cannot use a temporary email address.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous ne pouvez pas utiliser une adresse email temporaire.\"\n          }\n        }\n      }\n    },\n    \"You cannot view the communities of a private instance.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous ne pouvez pas voir les communautés d'une instance privée.\"\n          }\n        }\n      }\n    },\n    \"You don't have an email attached to this account.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous n'avez pas d'e-mail associé à ce compte.\"\n          }\n        }\n      }\n    },\n    \"You don't have any accounts.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous n'avez aucun compte.\"\n          }\n        }\n      }\n    },\n    \"You may need to allow Mlem to access your Photo Library in System Settings.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous devrez peut-être autoriser Mlem à accéder à votre galerie de photos dans les paramètres système.\"\n          }\n        }\n      }\n    },\n    \"You'll receive an email once your application has been approved. Once approved, you can log in to your account from the Settings tab.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vous recevrez un e-mail une fois votre demande approuvée. Une fois approuvée, vous pourrez vous connecter à votre compte depuis l'onglet des paramètres.\"\n          }\n        }\n      }\n    },\n    \"Your Answer...\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votre réponse...\"\n          }\n        }\n      }\n    },\n    \"Your instance and **%@** federate, but the content could not be loaded. It may not have federated yet, or your instance may have purged it.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votre instance et **%@** se fédèrent, mais le contenu n'a pas pu être chargé. Il se peut qu'il n'ait pas encore été fédéré ou que votre instance l'ait purgé.\"\n          }\n        }\n      }\n    },\n    \"Your instance, **%@**, chose to defederate from **%@**.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Your instance, **%1$@**, chose to defederate from **%2$@**.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votre instance, **%1$@**, a choisi de se défédérer de **%2$@**.\"\n          }\n        }\n      }\n    },\n    \"Your instance, **%@**, hasn't chosen to federate with **%@**.\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Your instance, **%1$@**, hasn't chosen to federate with **%2$@**.\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votre instance, **%1$@**, n'a pas choisi de se fédérer avec **%2$@**.\"\n          }\n        }\n      }\n    },\n    \"Your saved posts and comments\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vos publications et commentaires enregistrés\"\n          }\n        }\n      }\n    },\n    \"Your session has expired. Enter your password to authenticate a new session.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Votre session a expiré. Saisissez votre mot de passe pour authentifier une nouvelle session.\"\n          }\n        }\n      }\n    },\n    \"Your subscriptions live here.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vos abonnements sont ici.\"\n          }\n        }\n      }\n    },\n    \"Zoom Indicator\" : {\n\n    },\n    \"Zoom the image viewer with a slide gesture on the selected side.\" : {\n      \"localizations\" : {\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Zoomez la visionneuse d'images avec un geste de glissement sur le côté sélectionné.\"\n          }\n        }\n      }\n    }\n  },\n  \"version\" : \"1.0\"\n}"
  },
  {
    "path": "Mlem/Mlem.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.network.client</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n\t<key>keychain-access-groups</key>\n\t<array>\n\t\t<string>$(AppIdentifierPrefix)com.hanners.Mlem</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "Mlem/Mlem.xcdatamodeld/.xccurrentversion",
    "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</plist>\n"
  },
  {
    "path": "Mlem/Mlem.xcdatamodeld/Mlem.xcdatamodel/contents",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<model type=\"com.apple.IDECoreDataModeler.DataModel\" documentVersion=\"1.0\" lastSavedToolsVersion=\"1\" systemVersion=\"11A491\" minimumToolsVersion=\"Automatic\" sourceLanguage=\"Swift\" usedWithCloudKit=\"false\" userDefinedModelVersionIdentifier=\"\">\n    <entity name=\"Item\" representedClassName=\"Item\" syncable=\"YES\" codeGenerationType=\"class\">\n        <attribute name=\"timestamp\" optional=\"YES\" attributeType=\"Date\" usesScalarValueType=\"NO\"/>\n    </entity>\n    <elements>\n        <element name=\"Item\" positionX=\"-63\" positionY=\"-18\" width=\"128\" height=\"44\"/>\n    </elements>\n</model>"
  },
  {
    "path": "Mlem/Packages/Actions/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"Actions\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"Actions\",\n            targets: [\"Actions\"]\n        )\n\n    ],\n    dependencies: [\n        .package(path: \"../Icons\"),\n        .package(path: \"../Theming\")\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"Actions\",\n            dependencies: [\n                .byName(name: \"Icons\"),\n                .byName(name: \"Theming\")\n            ],\n            swiftSettings: [\n                .swiftLanguageMode(.v5),\n                .enableUpcomingFeature(\"BareSlashRegexLiterals\")\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/Actions/Sources/Actions/Action.swift",
    "content": "//\n//  File.swift\n//  Actions\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport SwiftUI\n\npublic protocol Action {\n    func createLabel(environment: EnvironmentValues) -> ActionLabel\n    \n    @MainActor\n    func execute(environment: EnvironmentValues)\n}\n"
  },
  {
    "path": "Mlem/Packages/Actions/Sources/Actions/ActionLabel.swift",
    "content": "//\n//  File.swift\n//  Actions\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport Foundation\nimport Icons\nimport Theming\n\npublic struct ActionLabel {\n    public var title: String\n    public var icon: Icon\n    public var color: ThemedColor\n    public var isDestructive: Bool\n    public var visibility: ActionVisiblity\n    \n    public init(\n        _ title: LocalizedStringResource,\n        icon: Icon,\n        color: ThemedColor = .themedAccent,\n        isDestructive: Bool = false,\n        visibility: ActionVisiblity = .enabled\n    ) {\n        self.title = .init(localized: title)\n        self.icon = icon\n        self.color = color\n        self.isDestructive = isDestructive\n        self.visibility = visibility\n    }\n    \n    @_disfavoredOverload\n    public init(\n        _ title: some StringProtocol,\n        icon: Icon,\n        color: ThemedColor = .themedAccent,\n        isDestructive: Bool = false,\n        visibility: ActionVisiblity = .enabled\n    ) {\n        self.title = String(title)\n        self.icon = icon\n        self.color = color\n        self.isDestructive = isDestructive\n        self.visibility = visibility\n    }\n    \n    public func withVisibility(_ visibility: ActionVisiblity) -> ActionLabel {\n        var new = self\n        new.visibility = visibility\n        return new\n    }\n\n    public func withTitle(_ title: LocalizedStringResource) -> ActionLabel {\n        var new = self\n        new.title = .init(localized: title)\n        return new\n    }\n\n    @_disfavoredOverload\n    public func withTitle(_ title: some StringProtocol) -> ActionLabel {\n        var new = self\n        new.title = String(title)\n        return new\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Actions/Sources/Actions/ActionSeed.swift",
    "content": "//\n//  ActionSeed.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-10-16.\n//\n\nimport Foundation\n\npublic final class ActionSeed: Hashable, Encodable {\n    public let key: String\n    private let actionType: any Action.Type\n    \n    public let label: ActionLabel\n    public let createAction: (Any) -> (any Action)?\n    \n    public init<T: Action>(\n        _ key: String,\n        label: ActionLabel,\n        createAction: @escaping (Any) -> T?\n    ) {\n        self.key = key\n        self.label = label\n        self.createAction = createAction\n        self.actionType = T.self\n    }\n\n    public convenience init<T: SimpleLabelAction>(\n        _ key: String,\n        createAction: @escaping (Any) -> T?\n    ) {\n        self.init(key, label: T.label, createAction: createAction)\n    }\n    \n    public static func == (lhs: ActionSeed, rhs: ActionSeed) -> Bool {\n        lhs.key == rhs.key\n    }\n    \n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(key)\n    }\n\n    public func encode(to encoder: any Encoder) throws {\n        try self.key.encode(to: encoder)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Actions/Sources/Actions/ActionVisiblity.swift",
    "content": "//\n//  File.swift\n//  Actions\n//\n//  Created by Sjmarf on 2025-10-14.\n//\n\nimport Foundation\n\npublic enum ActionVisiblity {\n    case enabled, disabled, hidden\n}\n"
  },
  {
    "path": "Mlem/Packages/Actions/Sources/Actions/BasicAction.swift",
    "content": "//\n//  File.swift\n//  Actions\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport Icons\nimport SwiftUI\n\n/// A basic implementation of `Action` designed for use in non-customizable contexts, such as the options within an alert.\n///\n/// ```swift\n/// BasicAction(\"Confirm\", icon: .general.success) {\n///       post.upvote()\n/// }\n/// ```\n///\npublic struct BasicAction: Action {\n    private let label: ActionLabel\n    private let callback: () -> Void\n    \n    public init(\n        _ title: LocalizedStringResource,\n        icon: Icon,\n        callback: @escaping () -> Void\n    ) {\n        self.label = .init(title, icon: icon)\n        self.callback = callback\n    }\n    \n    @_disfavoredOverload\n    public init(\n        _ title: String,\n        icon: Icon,\n        callback: @escaping () -> Void\n    ) {\n        self.label = .init(title, icon: icon)\n        self.callback = callback\n    }\n\n    public func createLabel(environment: EnvironmentValues) -> ActionLabel { label }\n    \n    public func execute(environment: EnvironmentValues) { callback() }\n}\n"
  },
  {
    "path": "Mlem/Packages/Actions/Sources/Actions/SimpleLabelAction.swift",
    "content": "//\n//  SimpleLabelAction.swift\n//  Actions\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport SwiftUI\n\npublic protocol SimpleLabelAction: Action {\n    static var label: ActionLabel { get }\n}\n\npublic extension SimpleLabelAction {\n    func createLabel(environment: EnvironmentValues) -> ActionLabel {\n        Self.label\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Actions/Sources/Actions/Views/ActionButtonWithVisibilityControl.swift",
    "content": "//\n//  File.swift\n//  Actions\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport SwiftUI\n\npublic struct ActionButtonWithVisibilityControl: View {\n    @Environment(\\.self) private var environment\n    \n    private let action: any Action\n    \n    public init(_ action: any Action) {\n        self.action = action\n    }\n    \n    public var body: some View {\n        let label = action.createLabel(environment: environment)\n        if label.visibility != .hidden {\n            Button(label) {\n                action.execute(environment: environment)\n            }\n            .disabled(label.visibility == .disabled)\n\n            // Without this, destructive items appear black in the\n            // subscription list due to a shim we've got in there #2374.\n            // Intentionally unthemed.\n            .tint(label.isDestructive ? .red : .primary)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Actions/Sources/Actions/Views/ActionButtons.swift",
    "content": "//\n//  File.swift\n//  Actions\n//\n//  Created by Sjmarf on 2025-10-14.\n//\n\nimport SwiftUI\n\n// This type can be used inside of a `.contextMenu()` rather than using the `ForEach` directly.\n// This avoids instantiating the actions until the menu is actually opened.\n\npublic struct ActionButtons: View {\n    @Environment(\\.self) var environment\n    \n    let actions: (EnvironmentValues) -> [any Actions.Action]\n    \n    public init(_ actions: @escaping (EnvironmentValues) -> [any Actions.Action]) {\n        self.actions = actions\n    }\n\n    public var body: some View {\n        ForEach(Array(actions(environment).enumerated()), id: \\.offset) { _, action in\n            ActionButtonWithVisibilityControl(action)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Actions/Sources/Actions/Views/Button+Extensions.swift",
    "content": "//\n//  Button+Extensions.swift\n//  Actions\n//\n//  Created by Sjmarf on 2025-11-12.\n//\n\nimport SwiftUI\n\npublic extension Button {\n    // Remeber to handle ActionLabel visibility when you use this\n    init(\n        _ label: ActionLabel,\n        callback: @escaping () -> Void\n    ) where Label == SwiftUI.Label<Text, Image> {\n        self.init(role: label.isDestructive ? .destructive : nil, action: callback) {\n            Label(label)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Actions/Sources/Actions/Views/Label+Extensions.swift",
    "content": "//\n//  File.swift\n//  Actions\n//\n//  Created by Sjmarf on 2025-10-13.\n//\n\nimport Icons\nimport SwiftUI\n\npublic extension Label where Title == Text, Icon == Image {\n    @inlinable\n    init(_ actionLabel: ActionLabel) {\n        self.init(actionLabel.title, icon: actionLabel.icon)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"ComponentViews\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"ComponentViews\",\n            targets: [\"ComponentViews\"]\n        )\n\n    ],\n    dependencies: [.package(path: \"../Theming\")],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"ComponentViews\",\n            dependencies: [.byName(name: \"Theming\")],\n            swiftSettings: [\n                .swiftLanguageMode(.v5),\n                .enableUpcomingFeature(\"BareSlashRegexLiterals\")\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/ButtonStyle/CapsuleButtonStyle.swift",
    "content": "//\n//  CapsuleButtonStyle.swift\n//  Mlem\n//\n//  Created by Sjmarf on 28/09/2024.\n//\n\nimport SwiftUI\n\npublic struct CapsuleButtonStyle: ButtonStyle {\n    @Environment(\\.palette) var palette\n    \n    public func makeBody(configuration: Self.Configuration) -> some View {\n        configuration.label\n            .fontWeight(.semibold)\n            .foregroundStyle(.themedAccent)\n            .padding(.vertical, 10)\n            .frame(maxWidth: .infinity)\n            .background {\n                Capsule()\n                    .fill(.themedSecondaryGroupedBackground)\n                    .stroke(palette.bordered ? .themedDivider : .clear, lineWidth: 0.5)\n            }\n            .opacity(configuration.isPressed ? 0.8 : 1)\n            .animation(.easeOut(duration: 0.1), value: configuration.isPressed)\n    }\n}\n\npublic extension ButtonStyle where Self == CapsuleButtonStyle {\n    @MainActor static var capsule: CapsuleButtonStyle { .init() }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/ButtonStyle/ChevronButtonStyle.swift",
    "content": "//\n//  ChevronButtonStyle.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-23.\n//\n\nimport Foundation\nimport SwiftUI\n\npublic struct ChevronButtonStyle: ButtonStyle {\n    public func makeBody(configuration: Self.Configuration) -> some View {\n        FormChevron {\n            configuration.label\n        }\n    }\n}\n\npublic extension ButtonStyle where Self == ChevronButtonStyle {\n    @MainActor static var chevron: ChevronButtonStyle { .init() }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/ButtonStyle/EmptyButtonStyle.swift",
    "content": "//\n//  Empty Button Style.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-06-10.\n//\n\nimport Foundation\nimport SwiftUI\n\n/// Style to disable navigation highlighting\npublic struct EmptyButtonStyle: ButtonStyle {\n    public func makeBody(configuration: Self.Configuration) -> some View {\n        configuration.label\n    }\n}\n\npublic extension ButtonStyle where Self == EmptyButtonStyle {\n    @MainActor static var empty: EmptyButtonStyle { .init() }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/Checkbox.swift",
    "content": "//\n//  Checkbox.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-01-05.\n//\n\nimport SwiftUI\nimport Theming\n\npublic struct Checkbox: View {\n    public let isOn: Bool\n    \n    public init(isOn: Bool) {\n        self.isOn = isOn\n    }\n    \n    public var body: some View {\n        VStack {\n            if isOn {\n                Image(systemName: \"checkmark.circle.fill\")\n                    .foregroundStyle(.themedContrastingLabel, .tint)\n                    .imageScale(.large)\n            } else {\n                Image(systemName: \"circle\")\n                    .foregroundStyle(.themedTertiary)\n                    .imageScale(.large)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/CloseButtonToolbarItem.swift",
    "content": "//\n//  File.swift\n//  ComponentViews\n//\n//  Created by Sjmarf on 2025-09-11.\n//\n\nimport SwiftUI\n\npublic struct CloseButtonToolbarItem: ToolbarContent {\n    var ios18Label: CloseButtonView.LabelType\n    var callback: (() -> Void)?\n    \n    public init(\n        ios18Label: CloseButtonView.LabelType = .xmark,\n        callback: (() -> Void)? = nil\n    ) {\n        self.ios18Label = ios18Label\n        self.callback = callback\n    }\n    \n    public var body: some ToolbarContent {\n        ToolbarItem(placement: placement) {\n            CloseButtonView(ios18Label: ios18Label, callback: callback)\n        }\n    }\n    \n    var placement: ToolbarItemPlacement {\n        if #available(iOS 26, *) {\n            return .topBarLeading\n        } else {\n            return .topBarTrailing\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/CloseButtonView.swift",
    "content": "//\n//  CloseButtonView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 09/03/2024.\n//\n\nimport Foundation\nimport SwiftUI\nimport Theming\n\npublic struct CloseButtonView: View {\n    @Environment(\\.dismiss) var dismiss\n    \n    public enum LabelType {\n        case cancel, xmark\n    }\n    \n    var ios18Label: LabelType\n    var requiresConfirmation: Bool\n    var callback: (() -> Void)?\n\n    @State var showingConfirmation: Bool = false\n    \n    public init(\n        ios18Label: LabelType = .xmark,\n        requiresConfirmation: Bool = false,\n        callback: (() -> Void)? = nil\n    ) {\n        self.ios18Label = ios18Label\n        self.requiresConfirmation = requiresConfirmation\n        self.callback = callback\n    }\n    \n    public var body: some View {\n        Group {\n            if #available(iOS 26, *) {\n                Button(\"Dismiss\", systemImage: \"xmark\", action: submit)\n                    .confirmationDialog(\"Really close?\", isPresented: $showingConfirmation) {\n                        Button(\"Yes\", role: .destructive, action: submit)\n                    } message: {\n                        Text(\"Really close?\")\n                    }\n            } else {\n                ios18Body\n                    .alert(\"Really close?\", isPresented: $showingConfirmation) {\n                        Button(\"Yes\", role: .destructive, action: submit)\n                        Button(\"Cancel\", role: .cancel) {}\n                    }\n            }\n        }\n    }\n    \n    @ViewBuilder\n    private var ios18Body: some View {\n        switch ios18Label {\n        case .cancel:\n            Button(\"Cancel\", action: submit)\n        case .xmark:\n            Button(action: submit) {\n                Image(systemName: \"xmark.circle.fill\")\n                    .resizable()\n                    .aspectRatio(contentMode: .fit)\n                    .frame(height: 30)\n                    .symbolRenderingMode(.palette)\n                    .foregroundStyle(.themedSecondary, .themedSecondary.opacity(0.2))\n            }\n            .buttonStyle(.plain)\n            .accessibilityLabel(\"Dismiss\")\n        }\n    }\n    \n    func submit() {\n        if requiresConfirmation, !showingConfirmation {\n            showingConfirmation = true\n        } else if let callback {\n            callback()\n        } else {\n            dismiss()\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/CollapsibleSheetView.swift",
    "content": "//\n//  CollapsibleSheetView.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/07/2024.\n//\n\nimport SwiftUI\n\npublic struct CollapsibleSheetView<Content: View>: View {\n    let content: Content\n    let canDismiss: Bool\n    \n    @Binding var presentationSelection: PresentationDetent\n    \n    public init(\n        presentationSelection: Binding<PresentationDetent>,\n        canDismiss: Bool,\n        @ViewBuilder content: () -> Content\n    ) {\n        self.canDismiss = canDismiss\n        self._presentationSelection = presentationSelection\n        self.content = content()\n    }\n\n    public var body: some View {\n        content\n            .opacity(presentationSelection == .large ? 1 : 0)\n            .overlay(alignment: .top) {\n                Button {\n                    presentationSelection = .large\n                } label: {\n                    Image(systemName: \"chevron.compact.up\")\n                        .resizable()\n                        .aspectRatio(contentMode: .fit)\n                        .frame(height: 16)\n                        .foregroundStyle(.themedSecondary)\n                        .frame(maxWidth: .infinity, maxHeight: 62)\n                        .contentShape(.rect)\n                }\n                .opacity(presentationSelection == .large ? 0 : 1)\n            }\n            .animation(.easeOut(duration: 0.2), value: presentationSelection)\n            .presentationDetents(canDismiss ? [.large] : [.height(62), .large], selection: $presentationSelection)\n            .interactiveDismissDisabled(!canDismiss)\n            .presentationCornerRadius(presentationSelection == .large ? nil : 16)\n            .presentationBackgroundInteraction(.enabled)\n            .presentationDragIndicator(.hidden)\n            .background(.themedBackground)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/FitContentPresentationDetentViewModifier.swift",
    "content": "//\n//  File.swift\n//  ComponentViews\n//\n//  Created by Sjmarf on 2025-06-15.\n//\n\nimport SwiftUI\n\nprivate struct FitContentPresentationDetentViewModifier: ViewModifier {\n    let otherDetents: Set<PresentationDetent>\n    var selection: Binding<PresentationDetent>?\n    \n    @State private var sheetContentHeight: CGFloat = SheetHeightKey.defaultValue\n    \n    func body(content: Content) -> some View {\n        if let selection, !otherDetents.isEmpty {\n            innerBody(content: content)\n                .presentationDetents(\n                    otherDetents.union([.height(sheetContentHeight)]),\n                    selection: selection\n                )\n        } else {\n            innerBody(content: content)\n                .presentationDetents(otherDetents.union([.height(sheetContentHeight)]))\n        }\n    }\n\n    func innerBody(content: Content) -> some View {\n        content\n            .overlay {\n                GeometryReader { proxy in\n                    Color.clear.preference(\n                        key: SheetHeightKey.self,\n                        value: proxy.size.height\n                    )\n                }\n            }\n            .onPreferenceChange(SheetHeightKey.self) { sheetContentHeight = $0 }\n    }\n}\n\npublic extension View {\n    @ViewBuilder\n    func presentationDetentFitsContent(\n        fitDetentEnabled: Bool = true,\n        _ otherDetents: Set<PresentationDetent> = []\n    ) -> some View {\n        if fitDetentEnabled {\n            modifier(FitContentPresentationDetentViewModifier(otherDetents: otherDetents))\n        } else {\n            presentationDetents(otherDetents)\n        }\n    }\n\n    @ViewBuilder\n    func presentationDetentFitsContent(\n        fitDetentEnabled: Bool = true,\n        _ otherDetents: Set<PresentationDetent> = [],\n        selection: Binding<PresentationDetent>\n    ) -> some View {\n        if fitDetentEnabled {\n            modifier(FitContentPresentationDetentViewModifier(otherDetents: otherDetents, selection: selection))\n        } else {\n            presentationDetents(otherDetents, selection: selection)\n        }\n    }\n}\n\nprivate struct SheetHeightKey: PreferenceKey {\n    static var defaultValue: CGFloat = 500\n    \n    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {\n        value = nextValue()\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/FormChevron.swift",
    "content": "//\n//  FormChevron.swift\n//  Mlem\n//\n//  Created by Sjmarf on 25/08/2024.\n//\n\nimport SwiftUI\n\npublic struct FormChevron<Content: View>: View {\n    let content: Content\n    \n    public init(@ViewBuilder content: () -> Content) {\n        self.content = content()\n    }\n    \n    public var body: some View {\n        HStack(spacing: 0) {\n            content\n                .frame(maxWidth: .infinity, alignment: .leading)\n            Image(systemName: \"chevron.forward\")\n                .imageScale(.small)\n                .foregroundStyle(.themedTertiary)\n                .fontWeight(.semibold)\n        }\n        .contentShape(.rect)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/KeyboardAwarePadding.swift",
    "content": "//\n//  KeyboardAwarePadding.swift\n//  ComponentViews\n//\n//  Created by Sjmarf on 2025-05-27.\n//\n\nimport Combine\nimport SwiftUI\n\n// https://stackoverflow.com/a/59098816/17629371\nstruct KeyboardAwareModifier: ViewModifier {\n    let removePaddingOnDismiss: Bool\n    \n    @State private var keyboardHeight: CGFloat = 0\n\n    private var keyboardHeightPublisher: AnyPublisher<CGFloat, Never> {\n        Publishers.Merge(\n            NotificationCenter.default\n                .publisher(for: UIResponder.keyboardWillShowNotification)\n                .compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue }\n                .map(\\.cgRectValue.height),\n            NotificationCenter.default\n                .publisher(for: UIResponder.keyboardWillHideNotification)\n                .map { _ in CGFloat(0) }\n        ).eraseToAnyPublisher()\n    }\n\n    func body(content: Content) -> some View {\n        content\n            .padding(.bottom, keyboardHeight)\n            .onReceive(keyboardHeightPublisher) {\n                if removePaddingOnDismiss || $0 > 0 {\n                    keyboardHeight = $0\n                }\n            }\n    }\n}\n\npublic extension View {\n    func keyboardAwarePadding(removePaddingOnDismiss: Bool = true) -> some View {\n        ModifiedContent(content: self, modifier: KeyboardAwareModifier(removePaddingOnDismiss: removePaddingOnDismiss))\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/Line.swift",
    "content": "//\n//  Line.swift\n//  Mlem\n//\n//  Created by Sjmarf on 09/06/2024.\n//\n\nimport SwiftUI\n\npublic struct Line: Shape {\n    public init() {}\n    \n    public func path(in rect: CGRect) -> Path {\n        var path = Path()\n        path.move(to: CGPoint(x: 0, y: 0))\n        path.addLine(to: CGPoint(x: rect.width, y: 0))\n        return path\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/MockTextView.swift",
    "content": "//\n//  MockTextView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-01-27.\n//\n\nimport SwiftUI\nimport Theming\n\npublic struct MockTextView: View {\n    @Environment(\\.palette) var palette\n    \n    let beginOpacity: CGFloat\n    let endOpacity: CGFloat\n    \n    public init(beginOpacity: CGFloat? = nil, endOpacity: CGFloat? = nil) {\n        self.beginOpacity = beginOpacity ?? 0.55\n        self.endOpacity = endOpacity ?? 0.45\n    }\n    \n    public var body: some View {\n        Capsule()\n            .fill(LinearGradient(\n                colors: [\n                    palette.label.secondary.opacity(beginOpacity),\n                    palette.label.secondary.opacity(endOpacity)\n                ],\n                startPoint: .leading,\n                endPoint: .trailing\n            ))\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/OptimalHeightLayout.swift",
    "content": "//\n//  OptimalHeightLayout.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-23.\n//\n\nimport SwiftUI\n\n// https://stackoverflow.com/a/77631512/17629371\npublic struct OptimalHeightLayout: Layout {\n    public init() {}\n    \n    public func sizeThatFits(\n        proposal: ProposedViewSize,\n        subviews: Subviews,\n        cache: inout ()\n    ) -> CGSize {\n        let result: CGSize\n        if let firstSubview = subviews.first {\n            let containerWidth = proposal.width ?? .infinity\n            let size = firstSubview.sizeThatFits(.init(width: containerWidth, height: nil))\n            result = CGSize(width: containerWidth, height: size.height)\n        } else {\n            result = .zero\n        }\n        return result\n    }\n\n    public func placeSubviews(\n        in bounds: CGRect,\n        proposal: ProposedViewSize,\n        subviews: Subviews,\n        cache: inout ()\n    ) {\n        if let firstSubview = subviews.first {\n            firstSubview.place(\n                at: CGPoint(x: bounds.minX, y: bounds.minY),\n                proposal: .init(width: bounds.width, height: bounds.height)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/View+GlassProminentButtonStyle.swift",
    "content": "//\n//  File.swift\n//  ComponentViews\n//\n//  Created by Sjmarf on 2025-09-11.\n//\n\nimport SwiftUI\n\npublic extension View {\n    @ViewBuilder\n    func glassProminentButtonStyle() -> some View {\n        if #available(iOS 26, *) {\n            buttonStyle(.glassProminent)\n        } else {\n            self\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/ComponentViews/Sources/ComponentViews/View+VersionAwareDialog.swift",
    "content": "//\n//  View+VersionAwareDialog.swift\n//  ComponentViews\n//\n//  Created by Sjmarf on 2025-08-23.\n//\n\nimport SwiftUI\n\n// Acts as a `.alert()` on iOS 26 and above and a `.confirmationDialog()` otherwise\npublic extension View {\n    @ViewBuilder\n    func versionAwareDialog(\n        _ title: LocalizedStringResource,\n        isPresented: Binding<Bool>,\n        @ViewBuilder actions: @escaping () -> some View\n    ) -> some View {\n        versionAwareDialog(String(localized: title), isPresented: isPresented, actions: actions) {}\n    }\n    \n    @_disfavoredOverload @ViewBuilder\n    func versionAwareDialog(\n        _ title: String,\n        isPresented: Binding<Bool>,\n        @ViewBuilder actions: @escaping () -> some View\n    ) -> some View {\n        if #available(iOS 26, *) {\n            alert(title, isPresented: isPresented, actions: actions)\n        } else {\n            confirmationDialog(title, isPresented: isPresented, actions: actions) {\n                Text(title)\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func versionAwareDialog(\n        _ title: LocalizedStringResource,\n        isPresented: Binding<Bool>,\n        @ViewBuilder actions: @escaping () -> some View,\n        @ViewBuilder message: @escaping () -> some View\n    ) -> some View {\n        versionAwareDialog(String(localized: title), isPresented: isPresented, actions: actions, message: message)\n    }\n    \n    @_disfavoredOverload @ViewBuilder\n    func versionAwareDialog(\n        _ title: String,\n        isPresented: Binding<Bool>,\n        @ViewBuilder actions: @escaping () -> some View,\n        @ViewBuilder message: @escaping () -> some View\n    ) -> some View {\n        if #available(iOS 26, *) {\n            alert(title, isPresented: isPresented, actions: actions, message: message)\n        } else {\n            confirmationDialog(title, isPresented: isPresented, actions: actions, message: message)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/FediverseEvents/.gitignore",
    "content": ".DS_Store\n/.build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\n"
  },
  {
    "path": "Mlem/Packages/FediverseEvents/Package.resolved",
    "content": "{\n  \"originHash\" : \"433eb46e437fba071b47444bcf712c57872e1973de4812d146c7db18e50834e2\",\n  \"pins\" : [\n    {\n      \"identity\" : \"libwebp-xcode\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/libwebp-Xcode.git\",\n      \"state\" : {\n        \"revision\" : \"0d60654eeefd5d7d2bef3835804892c40225e8b2\",\n        \"version\" : \"1.5.0\"\n      }\n    },\n    {\n      \"identity\" : \"nuke\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/kean/Nuke.git\",\n      \"state\" : {\n        \"revision\" : \"0ead44350d2737db384908569c012fe67c421e4d\",\n        \"version\" : \"12.8.0\"\n      }\n    },\n    {\n      \"identity\" : \"sdwebimage\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/SDWebImage.git\",\n      \"state\" : {\n        \"revision\" : \"cac9a55a3ae92478a2c95042dcc8d9695d2129ca\",\n        \"version\" : \"5.21.0\"\n      }\n    },\n    {\n      \"identity\" : \"sdwebimagewebpcoder\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/SDWebImageWebPCoder\",\n      \"state\" : {\n        \"revision\" : \"f534cfe830a7807ecc3d0332127a502426cfa067\",\n        \"version\" : \"0.14.6\"\n      }\n    }\n  ],\n  \"version\" : 3\n}\n"
  },
  {
    "path": "Mlem/Packages/FediverseEvents/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"FediverseEvents\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"FediverseEvents\",\n            targets: [\"FediverseEvents\"]\n        )\n    ],\n    dependencies: [\n        .package(path: \"../MlemLogger\"),\n        .package(path: \"../Rest\")\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"FediverseEvents\",\n            dependencies: [\n                .byName(name: \"Rest\"),\n                .byName(name: \"MlemLogger\")\n            ],\n            swiftSettings: [\n                .swiftLanguageMode(.v5),\n                .enableUpcomingFeature(\"FullTypedThrows\"),\n                .enableUpcomingFeature(\"BareSlashRegexLiterals\")\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/FediverseEvents/Sources/FediverseEvents/Event.swift",
    "content": "//\n//  Event.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-23.\n//\n\nimport Foundation\n\npublic struct Event: Codable, Identifiable {\n    public let id: String\n    public let name: String\n    public let start: Date\n    public let end: Date\n    public let endpoints: EventEndpoints\n    public let logos: [EventLogo]\n    public let social: [EventSocial]\n}\n\npublic struct EventEndpoints: Codable {\n    public let open: URL?\n    public let auth_open: URL?\n    public let more_info: URL?\n}\n\npublic struct EventLogo: Codable {\n    public let type: String // Mime type, e.g. \"image/png\"\n    public let url: URL\n    public let size: String // E.g. \"512x512\"\n}\n\npublic struct EventSocial: Codable {\n    public let icon: EventSocialIcon\n    public let label: String\n    public let url: URL\n}\n\npublic enum EventSocialIcon: String, Codable {\n    case mastodon, lemmy, matrix, discord, other\n}\n"
  },
  {
    "path": "Mlem/Packages/FediverseEvents/Sources/FediverseEvents/EventsClient+Requests.swift",
    "content": "//\n//  EventsClient+Requests.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-23.\n//\n\nextension EventsClient {\n    public func listEvents() async throws -> [Event] {\n        let response = try await perform(ListEventsRequest())\n        return response.events\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/FediverseEvents/Sources/FediverseEvents/EventsClient.swift",
    "content": "//\n//  File.swift\n//  FediverseEvents\n//\n//  Created by Sjmarf on 2026-04-21.\n//  \n\nimport Foundation\nimport Observation\nimport Rest\n\npublic enum EventsEnvironment {\n    case qualityControl, production\n    \n    var address: URL {\n        switch self {\n        case .production: .init(string: \"https://api.fediverse.events\")!\n        case .qualityControl: .init(string: \"https://test-api.fediverse.events\")!\n        }\n    }\n}\n\n@Observable\npublic final class EventsClient {\n    internal let restClient = RestClient(convertParamsToSnakeCase: false, decoder: .defaultDecoder)\n\n    public internal(set) var environment: EventsEnvironment = .production\n\n    internal var baseUrl: URL { environment.address }\n\n    public init() {}\n\n    public func changeEnvironment(to environment: EventsEnvironment) {\n        self.environment = environment\n    }\n\n    @discardableResult\n    internal func perform<Request: RestRequest>(_ request: Request) async throws -> Request.Response {\n        return try await restClient.perform(baseUrl: baseUrl, request, token: nil)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/FediverseEvents/Sources/FediverseEvents/Requests/ListEventsRequest.swift",
    "content": "//\n//  ListEventsRequest.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-04-23.\n//\n\nimport Rest\n\ninternal struct ListEventsRequest: GetRequest {\n    typealias Parameters = Never\n    \n    let path: String = \"v1/events\"\n    let parameters: Parameters? = nil\n\n    struct Response: Codable {\n        let events: [Event]\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"Haptics\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"Haptics\",\n            targets: [\"Haptics\"]\n        )\n\n    ],\n    dependencies: [\n        .package(path: \"../MlemLogger\")\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"Haptics\",\n            dependencies: [\n                .byName(name: \"MlemLogger\")\n            ],\n            resources: [.process(\"Resources\")],\n            swiftSettings: [\n                .swiftLanguageMode(.v5),\n                .enableUpcomingFeature(\"BareSlashRegexLiterals\")\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/Haptic.swift",
    "content": "//\n//  Haptic.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-08-02.\n//\n\nimport Foundation\n\n/// Enumerates all custom defined haptics used in the app. The raw value of a case corresponds to the name of the file it is stored in.\npublic enum Haptic: String, CaseIterable {\n    /// Very gentle tap. Used for subtle feedback--things like crossing a swipe boundary\n    case gentleInfo = \"Gentle Info\"\n    \n    /// Slightly firmer tap. Used for less subtle feedback--crossing a second swipe boundary, dropping a widget\n    case firmInfo = \"Firm Info\"\n\n    /// Mushy, gentle tap. Used for extremely subtle feedback\n    case mushyInfo = \"Mushy Info\"\n    \n    /// Rigid tap. Used for subtle feedback on \"clickier\" things\n    case rigidInfo = \"Rigid Info\"\n    \n    /// Success notification for extremely common, low-priority successes--dropping a widget, upvoting a post\n    case lightSuccess = \"Light Success\"\n    \n    /// Standard success notification\n    /// NOTE: this is a gentleInfo and a firmerInfo played in quick succession\n    case success = \"Success\"\n    \n    /// Success notification for destructive events like unsubscribing or deleting\n    case destructiveSuccess = \"Destructive Success\"\n    \n    /// Success notification for events like blocking a user, sending a report\n    case violentSuccess = \"Violent Success\"\n    \n    /// Failure notification\n    case failure = \"Failure\"\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/HapticError.swift",
    "content": "//\n//  File.swift\n//  Haptics\n//\n//  Created by Sjmarf on 2025-05-28.\n//\n\nimport Foundation\n\npublic enum HapticError: Error, CustomStringConvertible {\n    case failedToStartEngine(Error)\n    case failedToStartPlayer(Error)\n    case failedToMakePlayer(Error)\n    case noPlayer(Haptic)\n    \n    public var description: String {\n        switch self {\n        case let .failedToStartEngine(error):\n            \"HapticManager engine failed to start. Underlying error: \\(String(describing: error))\"\n        case let .failedToStartPlayer(error):\n            \"HapticManager player failed to start. Underlying error: \\(String(describing: error))\"\n        case let .failedToMakePlayer(error):\n            \"HapticManager failed to make player. Underlying error: \\(String(describing: error))\"\n        case let .noPlayer(haptic):\n            \"No player available for \\(haptic.rawValue)\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/HapticLevel.swift",
    "content": "//\n//  HapticPriority.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-10.\n//\n\nimport Foundation\n\npublic enum HapticTier: String, CaseIterable, Comparable, Codable {\n    case high\n    case low\n    \n    var intValue: Int {\n        switch self {\n        case .high: return 2\n        case .low: return 1\n        }\n    }\n    \n    public static func < (lhs: HapticTier, rhs: HapticTier) -> Bool { lhs.intValue < rhs.intValue }\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/HapticManager.swift",
    "content": "//\n//  HapticManager.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-07-17.\n//\n\nimport AVFAudio\nimport CoreHaptics\nimport Foundation\nimport MlemLogger\nimport os\nimport SwiftUI\n\n@Observable\npublic class HapticManager {\n    private let log: Logger = .mlemLogger()\n    \n    static let mainInternal: HapticManager = .init()\n    \n    @available(*, deprecated, message: \"Access the HapticManager from the environment instead.\")\n    public static var main: HapticManager { mainInternal }\n    \n    // Config\n    var errorHandler: (HapticError) -> Void = { Logger.universal.error(\"Haptic error: \\($0.description)\") }\n    var maximumHapticTier: HapticTier?\n    \n    // generators/engines\n    private let rigidImpactGenerator: UIImpactFeedbackGenerator = .init(style: .rigid)\n    private let notificationGenerator: UINotificationFeedbackGenerator = .init()\n    private var hapticEngine: CHHapticEngine?\n    \n    private var players: [Haptic: CHHapticPatternPlayer] = .init()\n    \n    private init() {\n        // create and start the engine if this device supports haptics\n        self.hapticEngine = initEngine()\n        \n        // load all the haptic files into players to avoid lag on first play caused by slow disk read\n        loadPlayers()\n        \n        // if the engine stops, tell us why\n        hapticEngine?.stoppedHandler = { reason in\n            self.log.warning(\"The engine stopped: \\(String(describing: reason))\")\n        }\n        \n        // if the engine fails, attempt to restart\n        hapticEngine?.resetHandler = { [weak self] in\n            (self?.log ?? Logger.universal).warning(\"The haptic engine reset\")\n            self?.handleFailure()\n        }\n        \n        log.info(\"Initialized haptic engine\")\n    }\n    \n    func startEngine() {\n        if let engine = hapticEngine {\n            do {\n                try engine.start()\n            } catch {\n                // silently log error, re-create the engine, and retry\n                errorHandler(.failedToStartEngine(error))\n                hapticEngine = initEngine()\n                loadPlayers()\n            }\n        }\n    }\n    \n    /// Plays a haptic if the given priority is equal to or lower than the current haptic level\n    public func play(haptic: Haptic, tier: HapticTier) {\n        Task(priority: .userInitiated) {\n            if hapticEngine == nil {\n                log.warning(\"\\(haptic.rawValue) not played (no engine)\")\n                return\n            }\n            \n            if tier.intValue <= (maximumHapticTier?.intValue ?? 0) {\n                do {\n                    guard let player = players[haptic] else { throw HapticError.noPlayer(haptic) }\n                    try player.start(atTime: .zero)\n                } catch {\n                    errorHandler(.failedToStartPlayer(error))\n                    handleFailure(with: haptic, error: error as? HapticError)\n                }\n            } else {\n                log.debug(\"\\(haptic.rawValue) not played (priority \\(tier.intValue) > \\(self.maximumHapticTier?.intValue ?? 0))\")\n            }\n        }\n    }\n    \n    /// If this device supports haptics, creates and returns a CHHaptic engine; otherwise returns nil\n    private func initEngine() -> CHHapticEngine? {\n        if CHHapticEngine.capabilitiesForHardware().supportsHaptics {\n            do {\n                let ret = try CHHapticEngine(audioSession: AVAudioSession.sharedInstance())\n                try ret.start()\n                return ret\n            } catch {\n                errorHandler(.failedToStartEngine(error))\n            }\n        }\n        return nil\n    }\n    \n    /// Restarts the engine if it is present, creates it if not, starts the engine, and plays the given haptic\n    private func handleFailure(with haptic: Haptic? = nil, error: HapticError? = nil) {\n        if hapticEngine == nil {\n            hapticEngine = initEngine()\n        }\n        \n        if let error, case let .noPlayer(failedHaptic) = error {\n            assertionFailure(\"No player for \\(failedHaptic)\")\n            loadPlayers()\n        }\n        \n        if hapticEngine != nil {\n            startEngine()\n            \n            // attempt to play the pattern that failed, but don't do anything on failure here\n            if let haptic {\n                guard let player = players[haptic] else {\n                    assertionFailure(\"No player for \\(haptic) in failure handler\")\n                    errorHandler(.noPlayer(haptic))\n                    return\n                }\n                do {\n                    try player.start(atTime: .zero)\n                } catch {\n                    errorHandler(.failedToStartPlayer(error))\n                }\n            }\n        }\n    }\n    \n    private func loadPlayers() {\n        // load all the haptic files into players to avoid lag on first play caused by slow disk read\n        for haptic in Haptic.allCases {\n            do {\n                guard let path = Bundle.module.path(forResource: haptic.rawValue, ofType: \"ahap\") else {\n                    assertionFailure(\"No haptic file found for \\(haptic.rawValue)\")\n                    continue\n                }\n                let file = URL(filePath: path)\n                players[haptic] = try hapticEngine?.makePlayer(with: .init(contentsOf: file))\n            } catch {\n                assertionFailure(\"Failed to initialize haptic player\")\n                errorHandler(.failedToMakePlayer(error))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/Resources/Failure/Failure.ahap",
    "content": "{\n  \"Version\": 1,\n  \"Pattern\": [\n    {\n      \"Event\": {\n        \"Time\": 0,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.4\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 1.0\n          }\n        ]\n      }\n    },\n    {\n      \"Event\": {\n        \"Time\": 0.1,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.6\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.8\n          }\n        ]\n      }\n    },\n    {\n      \"Event\": {\n        \"Time\": 0.2,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.8\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.6\n          }\n        ]\n      }\n    },\n    {\n      \"Event\": {\n        \"Time\": 0.3,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 1.0\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.4\n          }\n        ]\n      }\n    }\n  ]\n}\n\n\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/Resources/Info/Firm Info.ahap",
    "content": "{\n  \"Version\": 1,\n  \"Pattern\": [\n    {\n      \"Event\": {\n        \"Time\": 0,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.6\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.75\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/Resources/Info/Gentle Info.ahap",
    "content": "{\n  \"Version\": 1,\n  \"Pattern\": [\n    {\n      \"Event\": {\n        \"Time\": 0,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.45\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.55\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/Resources/Info/Mushy Info.ahap",
    "content": "{\n  \"Version\": 1,\n  \"Pattern\": [\n    {\n      \"Event\": {\n        \"Time\": 0,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.45\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.2\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/Resources/Info/Rigid Info.ahap",
    "content": "{\n  \"Version\": 1,\n  \"Pattern\": [\n    {\n      \"Event\": {\n        \"Time\": 0,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 1\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 1\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/Resources/Success/Destructive Success.ahap",
    "content": "{\n  \"Version\": 1,\n  \"Pattern\": [\n    {\n      \"Event\": {\n        \"Time\": 0,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.7\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.8\n          }\n        ]\n      }\n    },\n    {\n      \"Event\": {\n        \"Time\": 0.2,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.9\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.6\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/Resources/Success/Light Success.ahap",
    "content": "{\n  \"Version\": 1,\n  \"Pattern\": [\n    {\n      \"Event\": {\n        \"Time\": 0,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.75\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.9\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/Resources/Success/Success.ahap",
    "content": "{\n  \"Version\": 1,\n  \"Pattern\": [\n    {\n      \"Event\": {\n        \"Time\": 0,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.45\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.55\n          }\n        ]\n      }\n    },\n    {\n      \"Event\": {\n        \"Time\": 0.1,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.75\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.9\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/Resources/Success/Violent Success.ahap",
    "content": "{\n  \"Version\": 1,\n  \"Pattern\": [\n    {\n      \"Event\": {\n        \"Time\": 0,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.8\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 1.0\n          }\n        ]\n      }\n    },\n    {\n      \"Event\": {\n        \"Time\": 0.55,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 0.4\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.5\n          }\n        ]\n      }\n    },\n    {\n      \"Event\": {\n        \"Time\": 0.75,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 1.0\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.4\n          }\n        ]\n      }\n    },\n    {\n      \"Event\": {\n        \"Time\": 0.76,\n        \"EventType\": \"HapticTransient\",\n        \"EventParameters\": [\n          {\n            \"ParameterID\": \"HapticIntensity\",\n            \"ParameterValue\": 1.0\n          },\n          {\n            \"ParameterID\": \"HapticSharpness\",\n            \"ParameterValue\": 0.6\n          }\n        ]\n      }\n    }\n  ]\n}\n\n"
  },
  {
    "path": "Mlem/Packages/Haptics/Sources/Haptics/View+Haptic.swift",
    "content": "//\n//  File.swift\n//  Haptics\n//\n//  Created by Sjmarf on 2025-05-28.\n//\n\nimport SwiftUI\n\nprivate struct HapticConfigurationViewModifier: ViewModifier {\n    @Environment(\\.scenePhase) var scenePhase\n        \n    func body(content: Content) -> some View {\n        content\n            .environment(HapticManager.mainInternal)\n            .onChange(of: scenePhase, initial: false) {\n                if scenePhase == .active {\n                    // When the app moves into the background, the haptic engine stops.\n                    // This ensures the engine is started before a haptic is played to avoid a short lag while the engine starts\n                    HapticManager.mainInternal.startEngine()\n                }\n            }\n    }\n}\n\npublic extension View {\n    func hapticConfiguration(\n        maximumHapticTier: HapticTier?,\n        errorHandler: @escaping (HapticError) -> Void\n    ) -> some View {\n        HapticManager.mainInternal.errorHandler = errorHandler\n        HapticManager.mainInternal.maximumHapticTier = maximumHapticTier\n        \n        return modifier(HapticConfigurationViewModifier())\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"Icons\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"Icons\",\n            targets: [\"Icons\"]\n        )\n\n    ],\n    dependencies: [],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"Icons\",\n            dependencies: [],\n            swiftSettings: [\n                .swiftLanguageMode(.v5),\n                .enableUpcomingFeature(\"BareSlashRegexLiterals\")\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/Icon+Fediseer.swift",
    "content": "//\n//  Icon+Fediseer.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-08.\n//\n\nimport Foundation\n\npublic extension Icon {\n    struct FediseerIcons {\n        public let fediseer: Icon = .init(\"shield.checkered\")\n        public let guarantee: Icon = .init(\"checkmark.seal\")\n        public let unguarantee: Icon = .init(\"xmark.seal\")\n        public let endorsement: Icon = .init(\"signature\")\n        public let hesitation: Icon = .init(\"exclamationmark.triangle\")\n        public let censure: Icon = .init(\"exclamationmark.octagon\")\n    }\n    \n    static let fediseer: FediseerIcons = .init()\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/Icon+General.swift",
    "content": "//\n//  File.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-06.\n//\n\nimport Foundation\n\npublic extension Icon {\n    struct GeneralIcons {\n        public let circle: Icon = .init(\"circle\")\n        \n        public let success: Icon = .applyCircle(\"checkmark\")\n        public let failure: Icon = .applyCircle(\"xmark\")\n        \n        public let warning: Icon = .init(\"exclamationmark.triangle\")\n        public let error: Icon = .init(\"exclamationmark.circle\")\n        \n        public let hide: Icon = .init(\"eye.slash\")\n        public let show: Icon = .init(\"eye\")\n        \n        public let time: Icon = .init(\"clock\")\n        public let updateTime: Icon = .init(\"clock.arrow.2.circlepath\")\n        \n        public let close: Icon = .applyCircle(\"multiply\")\n        public let add: Icon = .applyCircle(\"plus\")\n        public let remove: Icon = .applyCircle(\"minus\")\n        public let website: Icon = .init(\"globe\")\n        \n        public let undo: Icon = .applyCircle(\"arrow.uturn.backward\")\n        public let redo: Icon = .applyCircle(\"arrow.uturn.forward\")\n        \n        public let share: Icon = .init(\"square.and.arrow.up\")\n        public let search: Icon = .custom { variant in\n            switch variant {\n            case .active: \"text.magnifyingglass\"\n            default: \"magnifyingglass\"\n            }\n        }\n\n        public let settings: Icon = .init(\"gear\")\n        public let filter: Icon = .init(\"line.3.horizontal.decrease.circle\")\n        public let filterMenu: Icon = .init(.custom { _ in\n            if #available(iOS 26, *) {\n                \"line.3.horizontal.decrease\"\n            } else {\n                \"line.3.horizontal.decrease.circle\"\n            }\n        })\n        public let menu: Icon = .init(\"ellipsis\")\n        public let toolbarMenu: Icon = .init(.custom { _ in\n            if #available(iOS 26, *) {\n                \"ellipsis\"\n            } else {\n                \"ellipsis.circle\"\n            }\n        })\n        public let configure: Icon = .init(\"slider.horizontal.3\")\n        public let `import`: Icon = .init(\"square.and.arrow.down\")\n        public let export: Icon = .init(\"square.and.arrow.up\")\n        public let edit: Icon = .applyCircle(\"pencil\")\n        public let delete: Icon = .init(\"trash\")\n        public let undelete: Icon = .init(\"trash.slash\")\n        \n        public let copy: Icon = .init(\"doc.on.doc\")\n        public let paste: Icon = .init(\"doc.on.clipboard\")\n        public let signOut: Icon = .init(\"minus.circle\")\n        public let attachment: Icon = .init(\"paperclip\")\n        public let refresh: Icon = .init(\"arrow.clockwise\")\n        public let select: Icon = .init(\"selection.pin.in.out\")\n        \n        public let chooseFile: Icon = .init(\"folder\")\n        public let chooseImage: Icon = .init(\"photo\")\n        \n        public let image: Icon = .init(\"photo\")\n        public let photoLibary: Icon = .init(\"photo.on.rectangle.angled\")\n        public let createImage: Icon = .init(\"scanner\")\n        \n        public let play: Icon = .init(\"play\")\n        public let playCircle: Icon = .applyCircle(\"play.circle\")\n        public let pause: Icon = .init(\"pause\")\n\n        @inlinable public var muted: Icon { mute }\n        public let mute: Icon = .init(\"speaker.slash\")\n        public let unmute: Icon = .init(\"speaker.wave.2\")\n        \n        public let collapse: Icon = .custom { variant in\n            switch variant {\n            case .none: \"arrow.down.and.line.horizontal.and.arrow.up\"\n            case .active: \"minus.square.fill\"\n            case .inactive: \"minus.square\"\n            }\n        }\n\n        public let expand: Icon = .custom { variant in\n            switch variant {\n            case .none: \"arrow.up.and.line.horizontal.and.arrow.down\"\n            case .active: \"plus.square.fill\"\n            case .inactive: \"plus.square\"\n            }\n        }\n\n        public let embedding: Icon = .init(\"app.connected.to.app.below.fill\")\n        public let movie: Icon = .init(\"film\")\n        public let email: Icon = .init(\"envelope\")\n        public let action: Icon = .init(\"diamond\")\n        public let missing: Icon = .init(\"questionmark.square.dashed\")\n        public let connection: Icon = .init(\"antenna.radiowaves.left.and.right\")\n        public let haptics: Icon = .init(\"circle.dotted.and.circle\")\n        public let noWifi: Icon = .init(\"wifi.slash\")\n        public let browser: Icon = .init(\"safari\")\n        public let dropDown: Icon = .applyCircle(\"chevron.down\")\n        public let noFile: Icon = .init(\"questionmark.folder\")\n        public let forward: Icon = .init(\"chevron.forward\")\n        public let backward: Icon = .init(\"chevron.backward\")\n        public let security: Icon = .init(\"key\")\n        public let link: Icon = .init(\"link\")\n        public let info: Icon = .init(\"info.circle\")\n        \n        public let cloudflare: Icon = .init(\"cloud.bolt\")\n    }\n    \n    static let general: GeneralIcons = .init()\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/Icon+Lemmy.swift",
    "content": "//\n//  Icons+StaticValues.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-06.\n//\n\nimport Foundation\n\npublic extension Icon {\n    struct LemmyIcons {\n        // MARK: - Votes\n        \n        @inlinable public var upvoted: Icon { addUpvote }\n        @inlinable public var downvoted: Icon { addDownvote }\n        \n        public let addUpvote: Icon = .applySquare(\"arrow.up\")\n        public let addDownvote: Icon = .applySquare(\"arrow.down\")\n        \n        public let removeUpvote: Icon = .custom { variant in\n            switch variant {\n            case .active: \"minus.square.fill\"\n            case .inactive: \"minus.square\"\n            default: \"arrow.up.slash\"\n            }\n        }\n        \n        public let removeDownvote: Icon = .custom { variant in\n            switch variant {\n            case .active: \"minus.square.fill\"\n            case .inactive: \"minus.square\"\n            default: \"arrow.down.slash\"\n            }\n        }\n        \n        public let votes: Icon = .applySquare(\"arrow.up.arrow.down\")\n        \n        public let scoreCounter: Icon = .init(\"arrow.up.arrow.down.circle\")\n        public let upvoteCounter: Icon = .init(\"arrow.up.circle\")\n        public let downvoteCounter: Icon = .init(\"arrow.down.circle\")\n        \n        // MARK: - Reply/Send\n        \n        public let reply: Icon = .init(\"arrowshape.turn.up.left\")\n        public let replyCounter: Icon = .init(\"arrowshape.turn.up.left.circle\")\n        \n        public let send: Icon = .init(\"paperplane\")\n        public let sendMessage: Icon = .init(\"arrow.up\")\n        \n        // MARK: - Save\n        \n        @inlinable public var saved: Icon { addSave }\n        public let addSave: Icon = .init(\"bookmark\")\n        public let removeSave: Icon = .init(\"bookmark.slash\")\n        \n        // MARK: - Mark Read\n        \n        public let markRead: Icon = .init(\"envelope.open\")\n        public let markUnread: Icon = .init(\"envelope\")\n        \n        // MARK: - Block\n        \n        public let block: Icon = .init(\"hand.raised\")\n        public let unblock: Icon = .init(\"hand.raised.slash\")\n        \n        // MARK: - Pin\n        \n        @inlinable public var pinned: Icon { addPin }\n        public let addPin: Icon = .init(\"pin\")\n        public let removePin: Icon = .init(\"pin.slash\")\n        \n        // MARK: - Lock\n        \n        @inlinable public var locked: Icon { addLock }\n        public let addLock: Icon = .init(\"lock\")\n        public let removeLock: Icon = .init(\"lock.open\")\n        \n        // MARK: - Remove\n        \n        @inlinable public var removed: Icon { remove }\n        public let remove: Icon = .init(\"xmark.bin\")\n        public let restore: Icon = .init(\"arrow.up.bin\")\n        \n        // MARK: - Purge\n        \n        @inlinable public var purged: Icon { purge }\n        public let purge: Icon = .init(\"burn\")\n        \n        // MARK: - Ban\n\n        @inlinable public var bannedFromInstance: Icon { banFromInstance }\n        public let banFromInstance: Icon = .init(\"xmark.square\")\n        public let unbanFromInstance: Icon = .init(\"checkmark.square\")\n        @inlinable public var bannedFromCommunity: Icon { banFromCommunity }\n        public let banFromCommunity: Icon = .init(\"xmark.shield\")\n        public let unbanFromCommunity: Icon = .init(\"checkmark.shield\")\n\n        // MARK: - Subscribe\n\n        public let subscribed: Icon = .init(\"checkmark.circle\")\n        public let subscribe: Icon = .init(\"plus.circle\")\n        public let unsubscribe: Icon = .init(\"multiply.circle\")\n        public let didUnsubscribe: Icon = .init(\"person.slash\")\n        \n        // MARK: - Subscribe\n\n        @inlinable public var favorited: Icon { favorite }\n        public let favorite: Icon = .init(\"star\")\n        public let unfavorite: Icon = .init(\"star.slash\")\n\n        // MARK: - Collapse\n\n        public let collapseParent: Icon = .applySquare(\"chevron.up\")\n        public let collapseToTop: Icon = .applySquare(\"arrow.up.to.line\")\n\n        // MARK: - Moderation\n        \n        public let moderation: Icon = .init(\"shield\")\n        public let targetedPerson: Icon = .init(\"scope\")\n        public let administration: Icon = .init(\"crown\")\n        \n        @inlinable public var addModerator: Icon { moderation }\n        public let removeModerator: Icon = .init(\"shield.slash\")\n        \n        public let report: Icon = .init(\"flag\")\n        public let registrationApplication: Icon = .init(\"list.clipboard\")\n        public let modlog: Icon = .init(\"book.pages\")\n        public let transferCommunity: Icon = .init(\"arrow.right\")\n        \n        @inlinable public var addAdministrator: Icon { administration }\n        public let removeAdministrator: Icon = .init(\"arrowshape.down\")\n        \n        // MARK: - Inbox\n        \n        public let mention: Icon = .init(\"quote.bubble\")\n        public let message: Icon = .init(\"envelope\")\n        \n        // MARK: - Misc Post\n        \n        public let post: Icon = .init(\"doc.plaintext\")\n        public let comment: Icon = .init(\"bubble.left\")\n        public let crosspost: Icon = .applyCircle(\"shuffle\")\n        \n        @inlinable public var replies: Icon { comment }\n        public let unreadReplies: Icon = .init(\"text.bubble\")\n        \n        public let textPost: Icon = .init(\"text.book.closed\")\n        public let titleOnlyPost: Icon = .init(\"character.bubble\")\n        public let pollPost: Icon = .init(\"chart.bar.xaxis\")\n        \n        // MARK: - Feeds\n\n        public let feed: Icon = .init(\"scroll\")\n        public let federatedFeed: Icon = .init(\"circle.hexagongrid\")\n        public let localFeed: Icon = .init(\"building.2\")\n        public let subscribedFeed: Icon = .init(\"newspaper\")\n        public let popularFeed: Icon = .init(\"chart.line.uptrend.xyaxis\")\n        public let suggestedFeed: Icon = .init(\"lightbulb\")\n        @inlinable public var savedFeed: Icon { saved }\n        @inlinable public var moderatedFeed: Icon { moderation }\n\n        // MARK: - Sort Types\n\n        public let activeSort: Icon = .init(\"popcorn\")\n        public let hotSort: Icon = .init(\"flame\")\n        public let scaledSort: Icon = .init(\"arrow.up.left.and.down.right.and.arrow.up.right.and.down.left\")\n        public let newSort: Icon = .init(\"hare\")\n        public let oldSort: Icon = .init(\"tortoise\")\n        public let newCommentsSort: Icon = .init(\"exclamationmark.bubble\")\n        public let mostCommentsSort: Icon = .init(\"bubble.left.and.bubble.right\")\n        public let controversialSort: Icon = .init(\"bolt\")\n        public let topSort: Icon = .init(\"trophy\")\n        public let alphabeticalSort: Icon = .init(\"textformat\")\n        public let scoreSort: Icon = .init(\"star\")\n        public let usersSort: Icon = .init(\"person.2\")\n        public let versionSort: Icon = .init(\"server.rack\")\n        \n        // MARK: - Flairs\n\n        public let developerFlair: Icon = .init(\"hammer\")\n        public let botFlair: Icon = .init(\"terminal\")\n        public let opFlair: Icon = .init(\"person\")\n        public let newAccountFlair: Icon = .init(\"leaf\")\n        public let cakeDay: Icon = .init(\"birthday.cake\")\n        \n        // MARK: - General Concepts\n\n        public let federation: Icon = .init(\"point.3.filled.connected.trianglepath.dotted\")\n        public let `private`: Icon = .init(\"lock\")\n        public let captcha: Icon = .init(\"photo\")\n        public let instance: Icon = .init(\"building.2\")\n        public let community: Icon = .init(\"house\")\n        public let person: Icon = .init(\"person\")\n        public let inbox: Icon = .init(\"mail.stack\")\n        public let imageProxy: Icon = .init(\"firewall\")\n        public let subscriptionList: Icon = .init(\"list.bullet\")\n        public let tag: Icon = .init(\"tag\")\n        public let event: Icon = .init(\"crown\")\n        \n        @inlinable public var communityAvatar: Icon { community }\n        public let instanceAvatar: Icon = .init(\"building.2.crop.circle\")\n        public let personAvatar: Icon = .init(\"person.crop.circle\")\n\n        // MARK: - Other\n\n        public let noContent: Icon = .init(\"binoculars\")\n        public let note: Icon = .init(\"note.text\")\n        public let editNote: Icon = .init(\"square.and.pencil\")\n        public let openAccountSwitcher: Icon = .init(\"person.crop.rectangle.stack.fill\")\n        public let groupAccountSort: Icon = .init(\"square.stack.3d.up.fill\")\n        public let switchAccount: Icon = .init(\"arrow.left.arrow.right\")\n        public let switchAccountAndReload: Icon = .init(\"arrow.2.circlepath\")\n        public let switchAccountAndKeepPlace: Icon = .init(\"checkmark.diamond\")\n        public let visitInstance: Icon = .init(\"arrow.right\")\n        public let logIn: Icon = .init(\"person.text.rectangle\")\n        public let signUp: Icon = .init(\"pencil.and.list.clipboard\")\n        \n        public let jumpButton: Icon = .init(\"chevron.down\")\n        public let jumpToLastPositionButton: Icon = .init(\"chevron.down.2\")\n        \n        public let nsfwTag: Icon = .init(\"nsfw\", source: .custom)\n        \n        public func notificationCount(_ count: Int) -> Icon {\n            .init(count <= 50 ? \"\\(count).circle.fill\" : \"exclamationmark.circle.fill\")\n        }\n    }\n    \n    static let lemmy: LemmyIcons = .init()\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/Icon+Markdown.swift",
    "content": "//\n//  Icon+Markdown.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-07.\n//\n\nimport Foundation\n\npublic extension Icon {\n    struct MarkdownIcons {\n        public let bold: Icon = .init(\"bold\")\n        public let italic: Icon = .init(\"italic\")\n        public let strikethrough: Icon = .init(\"strikethrough\")\n        public let superscript: Icon = .init(\"textformat.superscript\")\n        public let `subscript`: Icon = .init(\"textformat.subscript\")\n        // Potentially \"chevron.left.chevron.right\" is better, it's iOS 18+ though\n        public let inlineCode: Icon = .init(\"chevron.left.forwardslash.chevron.right\")\n        public let quote: Icon = .init(\"quote.opening\")\n        public let heading: Icon = .init(\"textformat.size\")\n        public let spoiler: Icon = .init(\"eye\")\n        public let codeBlock: Icon = .init(\"text.viewfinder\")\n        \n        @inlinable public var insertLink: Icon { .general.link }\n        @inlinable public var uploadImage: Icon { .general.chooseImage }\n    }\n    \n    static let markdown: MarkdownIcons = .init()\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/Icon+Settings.swift",
    "content": "//\n//  Icon+Settings.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-06.\n//\n\nimport Foundation\n\npublic extension Icon {\n    struct SettingsIcons {\n        public let hideRead: Icon = .init(\"book\")\n        @inlinable public var showRead: Icon { hideRead }\n        \n        public let postSize: Icon = .init(\"rectangle.expand.vertical\")\n        public let postSizeCompact: Icon = .init(\"rectangle.grid.1x2\")\n        public let postSizeTiled: Icon = .init(\"rectangle.grid.2x2\")\n        public let postSizeHeadline: Icon = .init(\"rectangle\")\n        public let postSizeLarge: Icon = .init(\"text.below.photo\")\n        \n        public let blurNsfw: Icon = .init(\"eye.trianglebadge.exclamationmark\")\n        public let upvoteOnSave: Icon = .init(\"arrow.up.heart\")\n        public let readIndicatorSetting: Icon = .init(\"book\")\n        public let readIndicatorBarSetting: Icon = .init(\"rectangle.leftthird.inset.filled\")\n        public let profileTabSettings: Icon = .init(\"person.text.rectangle\")\n        public let nicknameField: Icon = .init(\"rectangle.and.pencil.and.ellipsis\")\n        public let label: Icon = .init(\"tag\")\n        public let unreadBadge: Icon = .init(\"envelope.badge\")\n        public let showAvatar: Icon = .init(\"person.fill.questionmark\")\n        public let thumbnail: Icon = .init(\"photo\")\n        public let author: Icon = .init(\"signature\")\n        public let leftRight: Icon = .init(\"arrow.left.arrow.right\")\n        public let developerMode: Icon = .init(\"wrench.adjustable.fill\")\n        public let limitImageHeightSetting: Icon = .init(\"rectangle.compress.vertical\")\n        public let appLockSettings: Icon = .init(\"lock.app.dashed\")\n        public let sidebar: Icon = .init(\"sidebar.left\")\n        public let infiniteScroll: Icon = .init(\"infinity\")\n        public let markReadOnScroll: Icon = .init(\"book\")\n        public let confirmImageUploads: Icon = .init(\"photo.badge.checkmark\")\n        public let swipeActions: Icon = .init(\"inset.filled.leadinghalf.rectangle\")\n        public let swipeAnywhere: Icon = .init(\"arrow.left\")\n        public let importSettings: Icon = .init(\"folder.badge.gearshape\")\n        public let inApp: Icon = .init(\"house\")\n        public let reader: Icon = .init(\"text.page\")\n        public let keywordFilter: Icon = .init(\"rectangle.and.text.magnifyingglass\")\n        public let saveSettings: Icon = .init(\"document.badge.gearshape\")\n        public let restoreSettings: Icon = .init(\"gearshape.arrow.trianglehead.2.clockwise.rotate.90\")\n        public let menuItems: Icon = .init(\"filemenu.and.selection\")\n        public let systemMode: Icon = .init(\"circle.lefthalf.filled\")\n        public let lightMode: Icon = .init(\"sun.max\")\n        public let darkMode: Icon = .init(\"moon\")\n        public let compactComments: Icon = .init(\"rectangle.compress.vertical\")\n        public let interactionBar: Icon = .init(\"square.and.line.vertical.and.square.fill\")\n        public let commentDepth: Icon = .init(\"text.append\")\n        public let qualifiedLabel: Icon = .init(\"at\")\n        public let right: Icon = .init(\"arrow.right\")\n        public let left: Icon = .init(\"arrow.left\")\n        public let center: Icon = .init(\"dot\")\n        public let zoomSlider: Icon = .init(\"arrow.up.and.down.and.sparkles\")\n        public let language: Icon = .init(\"globe\")\n        public let settingsIcons: Icon = .init(\"fleuron\")\n        public let privacy: Icon = .init(\"hand.raised\")\n        public let openExternalLinks: Icon = .init(\"arrow.up.right\")\n        public let tappableLinks: Icon = .init(\"hand.tap\")\n        public let general: Icon = .init(\"gear\")\n        public let safety: Icon = .init(\"shield.lefthalf.filled\")\n        public let accessibility: Icon = .init(\"hand.point.up.braille.fill\")\n        public let sorting: Icon = .init(\"arrow.up.and.down.text.horizontal\")\n        public let tabBar: Icon = .init(\"platter.filled.bottom.iphone\")\n        public let advanced: Icon = .init(\"gearshape.2.fill\")\n        public let localAccountOptions: Icon = .init(\"iphone\")\n        public let eula: Icon = .init(\"doc.plaintext\")\n        public let licence: Icon = .init(\"doc\")\n        public let ask: Icon = .init(\"questionmark.circle\")\n        public let jumpButton: Icon = .init(\"chevron.down.circle\")\n        public let longPress: Icon = .init(\"hand.point.up.left.fill\")\n        public let imageViewer: Icon = .init(\"rectangle.portrait.center.inset.filled\")\n        public let imageViewerControls: Icon = .init(\"ellipsis\")\n        public let imageViewerDismissSensitivity: Icon = .applySquare(\"arrow.down.to.line\")\n    }\n    \n    static let settings: SettingsIcons = .init()\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/Icon+Uptime.swift",
    "content": "//\n//  Icon+Uptime.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-08.\n//\n\nimport Foundation\n\npublic extension Icon {\n    struct UptimeIcons {\n        public let offline: Icon = .init(\"xmark.circle\")\n        public let online: Icon = .init(\"checkmark.circle\")\n        public let outage: Icon = .init(\"exclamationmark.circle\")\n    }\n    \n    static let uptime: UptimeIcons = .init()\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/Icon.swift",
    "content": "//\n//  Icon.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2024-12-23.\n//\n\nimport SwiftUI\n\npublic struct Icon: Hashable {\n    public enum Source {\n        case system, custom\n    }\n    \n    public enum Variant: Hashable {\n        case active, inactive\n    }\n    \n    public enum VariantApplicationStrategy {\n        case baseOnly(name: String)\n        case applySquare(name: String)\n        case applyCircle(name: String)\n        case custom((Variant?) -> String)\n        \n        // swiftlint:disable:next cyclomatic_complexity\n        func computeImageName(variant: Variant?) -> String {\n            switch self {\n            case let .baseOnly(name):\n                switch variant {\n                case .active: \"\\(name).fill\"\n                default: name\n                }\n            case let .applySquare(name):\n                switch variant {\n                case .inactive: \"\\(name).square\"\n                case .active: \"\\(name).square.fill\"\n                case nil: name\n                }\n            case let .applyCircle(name):\n                switch variant {\n                case .active: \"\\(name).circle.fill\"\n                case .inactive: \"\\(name).circle\"\n                case nil: name\n                }\n            case let .custom(value):\n                value(variant)\n            }\n        }\n    }\n    \n    let id: UUID\n    let variantApplicationStrategy: VariantApplicationStrategy\n    let source: Source\n    var appliedVariant: Variant?\n    \n    public init(_ variantApplicationStrategy: VariantApplicationStrategy, source: Source = .system) {\n        self.id = .init()\n        self.variantApplicationStrategy = variantApplicationStrategy\n        self.source = source\n    }\n    \n    public init(_ name: String, source: Source = .system) {\n        self.init(.baseOnly(name: name), source: source)\n    }\n    \n    public func computeImageName() -> String {\n        variantApplicationStrategy.computeImageName(variant: appliedVariant)\n    }\n    \n    public static func applySquare(_ name: String) -> Self {\n        self.init(.applySquare(name: name))\n    }\n    \n    public static func applyCircle(_ name: String) -> Self {\n        self.init(.applyCircle(name: name))\n    }\n    \n    public static func custom(_ customStrategy: @escaping (Variant?) -> String) -> Self {\n        self.init(.custom(customStrategy))\n    }\n    \n    private func applyVariant(_ newVariant: Variant) -> Icon {\n        var new = self\n        new.appliedVariant = newVariant\n        return new\n    }\n\n    public func representingState(active state: Bool) -> Icon {\n        applyVariant(state ? .active : .inactive)\n    }\n    \n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(id)\n        hasher.combine(appliedVariant)\n    }\n    \n    public static func == (lhs: Icon, rhs: Icon) -> Bool {\n        lhs.id == rhs.id && lhs.appliedVariant == rhs.appliedVariant\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/ViewExtensions/Button+Extensions.swift",
    "content": "//\n//  File.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-10.\n//\n\nimport SwiftUI\n\npublic extension Button where Label == SwiftUI.Label<Text, Image> {\n    nonisolated init(\n        _ title: LocalizedStringResource,\n        icon: Icon,\n        role: ButtonRole? = nil,\n        action: @escaping @MainActor () -> Void\n    ) {\n        self.init(LocalizedStringKey(title.key), systemImage: icon.computeImageName(), role: role, action: action)\n    }\n    \n    @_disfavoredOverload\n    nonisolated init(\n        _ title: String,\n        icon: Icon,\n        role: ButtonRole? = nil,\n        action: @escaping @MainActor () -> Void\n    ) {\n        self.init(title, systemImage: icon.computeImageName(), role: role, action: action)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/ViewExtensions/Image+Extensions.swift",
    "content": "//\n//  Image+Extensions.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-06.\n//\n\nimport SwiftUI\n\npublic extension Image {\n    init(icon: Icon) {\n        let name = icon.computeImageName()\n        switch icon.source {\n        case .custom:\n            self.init(name)\n        case .system:\n            self.init(systemName: name)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/ViewExtensions/Label+Extensions.swift",
    "content": "//\n//  File.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-10.\n//\n\nimport SwiftUI\n\npublic extension Label where Title == Text, Icon == Image {\n    init(_ title: LocalizedStringKey, icon: Icons.Icon) {\n        switch icon.source {\n        case .system:\n            self.init(title, systemImage: icon.computeImageName())\n        case .custom:\n            self.init(title, image: icon.computeImageName())\n        }\n    }\n    \n    @_disfavoredOverload\n    init(_ title: some StringProtocol, icon: Icons.Icon) {\n        switch icon.source {\n        case .system:\n            self.init(title, systemImage: icon.computeImageName())\n        case .custom:\n            self.init(title, image: icon.computeImageName())\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/ViewExtensions/Menu+Extensions.swift",
    "content": "//\n//  File.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-10.\n//\n\nimport SwiftUI\n\npublic extension Menu where Label == SwiftUI.Label<Text, Image> {\n    nonisolated init(_ title: LocalizedStringResource, icon: Icon, @ViewBuilder content: () -> Content) {\n        self.init(title.key, systemImage: icon.computeImageName(), content: content)\n    }\n    \n    @_disfavoredOverload\n    nonisolated init(_ title: some StringProtocol, icon: Icon, @ViewBuilder content: () -> Content) {\n        self.init(title, systemImage: icon.computeImageName(), content: content)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/ViewExtensions/Picker+Extensions.swift",
    "content": "//\n//  File.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-14.\n//\n\nimport SwiftUI\n\npublic extension Picker where Label == SwiftUI.Label<Text, Image> {\n    nonisolated init(\n        _ titleKey: LocalizedStringKey,\n        icon: Icon,\n        selection: Binding<SelectionValue>,\n        @ViewBuilder content: () -> Content\n    ) {\n        self.init(titleKey, systemImage: icon.computeImageName(), selection: selection, content: content)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/ViewExtensions/Toggle+Extensions.swift",
    "content": "//\n//  File.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-12.\n//\n\nimport SwiftUI\n\npublic extension Toggle where Label == SwiftUI.Label<Text, Image> {\n    nonisolated init(_ title: LocalizedStringResource, icon: Icon, isOn: Binding<Bool>) {\n        self.init(LocalizedStringKey(title.key), systemImage: icon.computeImageName(), isOn: isOn)\n    }\n    \n    @_disfavoredOverload\n    nonisolated init(_ title: some StringProtocol, icon: Icon, isOn: Binding<Bool>) {\n        self.init(title, systemImage: icon.computeImageName(), isOn: isOn)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Icons/Sources/Icons/ViewExtensions/UIImage+Extensions.swift",
    "content": "//\n//  File.swift\n//  Icons\n//\n//  Created by Sjmarf on 2025-04-12.\n//\n\nimport UIKit\n\npublic extension UIImage {\n    convenience init?(icon: Icon) {\n        self.init(systemName: icon.computeImageName())\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/.gitignore",
    "content": ".DS_Store\n/.build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\n"
  },
  {
    "path": "Mlem/Packages/Media/Package.resolved",
    "content": "{\n  \"originHash\" : \"9f566dd1a015f0bc4788be19b84e8a555f2d5656e28b7614517b9d04c3ddb2ba\",\n  \"pins\" : [\n    {\n      \"identity\" : \"collectionconcurrencykit\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/JohnSundell/CollectionConcurrencyKit.git\",\n      \"state\" : {\n        \"revision\" : \"b4f23e24b5a1bff301efc5e70871083ca029ff95\",\n        \"version\" : \"0.2.0\"\n      }\n    },\n    {\n      \"identity\" : \"libwebp-xcode\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/libwebp-Xcode.git\",\n      \"state\" : {\n        \"revision\" : \"0d60654eeefd5d7d2bef3835804892c40225e8b2\",\n        \"version\" : \"1.5.0\"\n      }\n    },\n    {\n      \"identity\" : \"nuke\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/kean/Nuke.git\",\n      \"state\" : {\n        \"revision\" : \"0ead44350d2737db384908569c012fe67c421e4d\",\n        \"version\" : \"12.8.0\"\n      }\n    },\n    {\n      \"identity\" : \"sdwebimage\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/SDWebImage.git\",\n      \"state\" : {\n        \"revision\" : \"cac9a55a3ae92478a2c95042dcc8d9695d2129ca\",\n        \"version\" : \"5.21.0\"\n      }\n    },\n    {\n      \"identity\" : \"sdwebimagewebpcoder\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/SDWebImageWebPCoder\",\n      \"state\" : {\n        \"revision\" : \"f534cfe830a7807ecc3d0332127a502426cfa067\",\n        \"version\" : \"0.14.6\"\n      }\n    },\n    {\n      \"identity\" : \"semaphore\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/groue/Semaphore.git\",\n      \"state\" : {\n        \"revision\" : \"2543679282aa6f6c8ecf2138acd613ed20790bc2\",\n        \"version\" : \"0.1.0\"\n      }\n    }\n  ],\n  \"version\" : 3\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"Media\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"Media\",\n            targets: [\"Media\"]\n        )\n    ],\n    dependencies: [\n        .package(url: \"https://github.com/kean/Nuke.git\", .upToNextMajor(from: \"12.6.0\")),\n        .package(url: \"https://github.com/SDWebImage/SDWebImageWebPCoder\", .upToNextMajor(from: \"0.14.6\")),\n        .package(path: \"../MlemMiddleware\"),\n        .package(path: \"../Rest\"),\n        .package(path: \"../Theming\")\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"Media\",\n            dependencies: [\n                .product(name: \"Nuke\", package: \"Nuke\"),\n                .product(name: \"SDWebImageWebPCoder\", package: \"SDWebImageWebPCoder\"),\n                .byName(name: \"MlemMiddleware\"),\n                .byName(name: \"Rest\"),\n                .byName(name: \"Theming\")\n            ],\n            swiftSettings: [.swiftLanguageMode(.v5)]\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/Media/Sources/Media/Resources/Core/Animated/AnimatedImageView.swift",
    "content": "//\n//  WebpView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-12-06.\n//\n\nimport MlemLogger\nimport os\nimport SDWebImage\nimport SwiftUI\n\n@preconcurrency\nstruct AnimatedImageView: UIViewRepresentable {\n    private let log: Logger = .mlemLogger()\n    \n    @Environment(MediaControlState.self) var controlState\n    \n    let data: Data\n    \n    @State var player: SDAnimatedImagePlayer?\n    @State var observer: NSKeyValueObservation?\n    \n    func makeUIView(context: Context) -> SDAnimatedImageView {\n        let imageView = SDAnimatedImageView()\n        imageView.autoPlayAnimatedImage = controlState.animating\n        \n        guard let animatedImage = SDAnimatedImage(data: data) else {\n            log.error(\"Could not create animated image\")\n            return imageView\n        }\n        \n        // gifs and webps can be \"animated\" with only 1 frame, in which case they should be treated as still images\n        guard animatedImage.animatedImageFrameCount > 1 else {\n            controlState.animationAvailable = false\n            return imageView\n        }\n        \n        if controlState.scrubbingAvailable {\n            // loads all frames, which enables smooth backwards scrubbing\n            Task {\n                animatedImage.preloadAllFrames()\n            }\n        }\n        \n        // compute real time duration\n        Task {\n            var total: TimeInterval = 0\n            for index in 0 ..< animatedImage.animatedImageFrameCount {\n                total += animatedImage.animatedImageDuration(at: index)\n            }\n            controlState.duration = total\n        }\n        \n        // set up player with observation to update controlState.playbackPosition\n        DispatchQueue.main.async {\n            guard let player = imageView.player else {\n                assertionFailure(\"ImageView had nil player\")\n                return\n            }\n            observer = player.observe(\\.currentFrameIndex) { player, _ in\n                controlState.playbackPosition = CGFloat(player.currentFrameIndex) / CGFloat(player.totalFrameCount)\n            }\n            self.player = player\n        }\n        \n        imageView.image = animatedImage\n        \n        // fit parent view\n        imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)\n        imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)\n        \n        return imageView\n    }\n    \n    func updateUIView(_ uiView: SDAnimatedImageView, context: Context) {\n        guard let player else {\n            return\n        }\n    \n        if let scrubTarget = controlState.scrubTarget {\n            if player.isPlaying {\n                player.pausePlaying()\n            }\n            \n            player.seekToFrame(\n                at: .init((scrubTarget * CGFloat(player.totalFrameCount)).rounded()),\n                loopCount: 0\n            )\n        } else if controlState.animating != player.isPlaying {\n            if controlState.animating {\n                player.startPlaying()\n            } else {\n                player.pausePlaying()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/Sources/Media/Resources/Core/Animated/VideoView.swift",
    "content": "//\n//  VideoView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-09-23.\n//\n\nimport AVFoundation\nimport AVKit\nimport MlemLogger\nimport NukeVideo\nimport os\nimport SwiftUI\n\nstruct VideoView: View {\n    private let log: Logger = .mlemLogger()\n    \n    @Environment(MediaControlState.self) var controlState\n    \n    let player: AVQueuePlayer\n    let playerLooper: AVPlayerLooper\n    \n    @State var timescale: CMTimeScale?\n    \n    var timer = Timer.publish(every: 0.02, on: .main, in: .common)\n        .autoconnect()\n    \n    init(asset: AVAsset) {\n        // set up AVQueuePlayer and AVPlayerLooper to loop the video\n        let playerItem: AVPlayerItem = .init(asset: asset)\n        \n        let player: AVQueuePlayer = .init(playerItem: playerItem)\n        self.player = player\n        self.playerLooper = .init(player: player, templateItem: playerItem)\n    }\n    \n    var body: some View {\n        VideoPlayer(player: player)\n            .disabled(true)\n            .task {\n                // parse whether the video has audio or not before playing so we can appropriately display audio controls\n                do {\n                    controlState.audioAvailable = try await player.isAudioAvailable() ?? false\n                } catch {\n                    log.error(\"\\(error.localizedDescription)\")\n                }\n            }\n            .task {\n                do {\n                    guard let asset = player.currentItem?.asset else {\n                        assertionFailure(\"Could not find AVAsset\")\n                        return\n                    }\n                    let cmTime = try await asset.load(.duration)\n                    controlState.duration = cmTime.seconds\n                    timescale = cmTime.timescale\n                } catch {\n                    log.error(\"\\(error.localizedDescription)\")\n                }\n            }\n            .onChange(of: controlState.animating, initial: true) {\n                if controlState.animating {\n                    player.play()\n                } else {\n                    player.pause()\n                }\n            }\n            .onChange(of: controlState.muted, initial: true) {\n                player.volume = controlState.muted ? 0 : 1\n            }\n            .onChange(of: controlState.scrubTarget) {\n                guard let timescale, let duration = controlState.duration, let playerItem = player.currentItem else {\n                    assertionFailure(\"Duration or playerItem not present\")\n                    return\n                }\n                if let target = controlState.scrubTarget {\n                    controlState.animating = false\n                    playerItem.seek(\n                        to: .init(seconds: target * duration, preferredTimescale: timescale),\n                        toleranceBefore: CMTime.zero,\n                        toleranceAfter: CMTime.zero,\n                        completionHandler: nil\n                    )\n                } else {\n                    controlState.animating = true\n                }\n            }\n            .onReceive(timer) { _ in\n                if let duration = controlState.duration, let playerItem = player.currentItem {\n                    let currentTime = playerItem.currentTime().seconds\n                    controlState.playbackPosition = currentTime / duration\n                }\n            }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/Sources/Media/Resources/Core/CoreMediaView+Logic.swift",
    "content": "//\n//  MediaView+Helpers.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-10.\n//\n\nimport Foundation\nimport SwiftUI\nimport Theming\n\npublic extension CoreMediaView {\n    // MARK: Types\n\n    enum AspectRatioBounds {\n        /// Specify an aspect ratio not taller than .vertical and not wider than the .horizontal\n        case bounded(vertical: CGSize?, horizontal: CGSize?)\n        /// Specify an exact aspect ratio\n        case absolute(CGSize)\n        \n        public var defaultSize: CGSize {\n            switch self {\n            case let .bounded(vertical, horizontal):\n                vertical ?? horizontal ?? .init(width: 1, height: 1)\n            case let .absolute(size):\n                size\n            }\n        }\n        \n        public var boundsAreSane: Bool {\n            switch self {\n            case let .bounded(vertical, horizontal):\n                if let vertical, let horizontal {\n                    // if both horizontal and vertical bound defined, ensure vertical bound taller than horizontal\n                    return vertical.aspectRatio > horizontal.aspectRatio\n                } else {\n                    return true\n                }\n            case .absolute:\n                return true\n            }\n        }\n        \n        public static var imageDefault: AspectRatioBounds { .bounded(vertical: .init(width: 4, height: 5), horizontal: nil) }\n        public static var absoluteSquare: AspectRatioBounds { .absolute(.init(width: 1, height: 1)) }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/Sources/Media/Resources/Core/CoreMediaView.swift",
    "content": "//\n//  CoreMediaView.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-04-20.\n//\n\nimport SwiftUI\n\n/// Struct to actually render the media.\n/// This is declared as its own struct to prevent state updates from the parent view causing unwanted behavior.\npublic struct CoreMediaView: View {\n    @Environment(MediaControlState.self) var controlState\n    \n    let media: MediaType\n    let aspectRatio: CGSize\n    let contentMode: ContentMode\n    \n    public init(media: MediaType, aspectRatio: CGSize, contentMode: ContentMode) {\n        self.media = media\n        self.aspectRatio = aspectRatio\n        self.contentMode = contentMode\n    }\n    \n    var uiImage: UIImage { media.image }\n\n    public var body: some View {\n        // WARNING: the combination of .aspectRatio and .frame modifiers in this view is very precise and\n        // breaks easily. If you have to modify it, be sure to thoroughly regression test!\n        // More info here: https://alejandromp.com/development/blog/image-aspectratio-without-frames/\n        Group {\n            if contentMode == .fit {\n                content\n            } else if contentMode == .fill {\n                content\n                    .frame(\n                        minWidth: 0,\n                        maxWidth: .infinity,\n                        minHeight: 0,\n                        maxHeight: .infinity\n                    )\n            }\n        }\n        .aspectRatio(aspectRatio, contentMode: contentMode)\n        .allowsHitTesting(false)\n    }\n    \n    @ViewBuilder\n    var content: some View {\n        if controlState.canAnimate, media.isAnimated {\n            animatedContent\n        } else {\n            Image(uiImage: uiImage)\n                .resizable()\n                .aspectRatio(contentMode: contentMode)\n        }\n    }\n    \n    @ViewBuilder\n    var animatedContent: some View {\n        Group {\n            switch media {\n            case let .video(_, animated):\n                VideoView(asset: animated)\n            case let .animated(_, animated):\n                AnimatedImageView(data: animated)\n            default:\n                EmptyView()\n            }\n        }\n        .aspectRatio(uiImage.size, contentMode: contentMode)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/Sources/Media/Resources/Core/MediaControlState.swift",
    "content": "//\n//  MediaControlState.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-02-05.\n//\n\nimport Foundation\nimport Observation\n\n@Observable\npublic class MediaControlState {\n    /// True if the media should be blurred, false otherwise\n    public var blurred: Bool\n    \n    /// True if the media, if animated, should be playing\n    public var animating: Bool\n    \n    /// True if the media, if animated, should autoplay; this is the initial value of `animating`\n    public let autoplay: Bool\n    \n    /// True if the media should animate, false to suppress animation\n    public var enableAnimation: Bool\n    \n    /// True if the media, if audio available, should not play audio\n    public var muted: Bool\n    \n    /// Target playback position of animated media\n    public var scrubTarget: CGFloat?\n    \n    /// True if the media is in a context where scrubbing is possible. Used to determine whether to aggressively\n    /// load image data into memory to improve scrubbing performance.\n    /// - Warning: This does NOT enable any form of scrubbing control! It only informs the underlying view whether to prepare\n    /// appropriately for scrubbing.\n    var scrubbingAvailable: Bool\n    \n    /// True if the media is animated.\n    /// - Note: This must be set by MediaView after the media type resolves\n    public var animationAvailable: Bool = false\n    \n    /// True when the media has an audio track, false otherwise.\n    /// - Note: This must be set by the relevant nested media view once it has extracted audio data\n    public var audioAvailable: Bool = false\n    \n    /// Current playback position of animated media\n    /// - Note: This should only be set by the nested media view; to scrub, update scrubTarget\n    public var playbackPosition: CGFloat = 0\n    \n    /// Duration of animated media\n    /// - Note: This should only be set by the nested media view\n    public var duration: TimeInterval?\n    \n    /// Current loading state of the media\n    public var loading: MediaLoadingState?\n    \n    public var playbackReadouts: (position: String, duration: String)? {\n        guard let duration else { return nil }\n        return (position: minuteSecondString(from: playbackPosition * duration), duration: minuteSecondString(from: duration))\n    }\n    \n    public var canAnimate: Bool { animationAvailable && enableAnimation }\n    \n    public var url: URL?\n    \n    /// Creates a new MediaControlState\n    /// - Parameters:\n    ///   - blurred: true if the media should be blurred\n    ///   - animating: true if animated media should currently be animating. If initialized with `true`, animated media will autoplay.\n    ///   - overlays: set of overlays to use\n    ///   - enableAnimation: true if the media should animate at all, false otherwise\n    ///   - muted: true if the media should be muted, false otherwise. Defaults to Settings.main.muteVideos.\n    ///   - audioAvailable: true if the media has an audio track, false otherwise. Defaults to false.\n    public init(\n        blurred: Bool,\n        animating: Bool,\n        enableAnimation: Bool = true,\n        muted: Bool,\n        scrubbingAvailable: Bool = false\n    ) {\n        self.blurred = blurred\n        self.animating = animating\n        self.autoplay = animating\n        self.enableAnimation = enableAnimation\n        self.muted = muted\n        self.scrubbingAvailable = scrubbingAvailable\n    }\n    \n    private func minuteSecondString(from timeInterval: TimeInterval) -> String {\n        Duration.seconds(timeInterval).formatted(.time(pattern: .minuteSecond))\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/Sources/Media/Resources/Core/MediaLoader.swift",
    "content": "//\n//  MediaLoader.swift\n//  Mlem\n//\n//  Created by Sjmarf on 10/08/2024.\n//\n\nimport AVFoundation\nimport Foundation\nimport MlemMiddleware\nimport Nuke\nimport Rest\nimport SwiftUI\n\n// MARK: Types\n\npublic enum ImageLoadingError {\n    case proxyFailure(proxyBypass: URL)\n    case error(error: Error)\n}\n\npublic enum MediaType {\n    case image(UIImage)\n    case video(still: UIImage, animated: AVAsset)\n    case animated(still: UIImage, animated: Data)\n    \n    public var image: UIImage {\n        switch self {\n        case let .image(image), let .video(image, _), let .animated(image, _): image\n        }\n    }\n    \n    public var isAnimated: Bool {\n        switch self {\n        case .image: false\n        default: true\n        }\n    }\n}\n\npublic enum MediaLoadingState {\n    case loading, done, proxyFailed, failed\n}\n\n// MARK: Core\n\n@Observable\npublic class MediaLoader {\n    public private(set) var url: URL?\n    public private(set) var mediaType: MediaType?\n    public private(set) var loading: MediaLoadingState\n    public private(set) var error: ImageLoadingError?\n    \n    @MainActor func setUrl(_ newValue: URL?) { url = newValue }\n    @MainActor func setMediaType(_ newValue: MediaType?) { mediaType = newValue }\n    @MainActor func setLoading(_ newValue: MediaLoadingState) { loading = newValue }\n    @MainActor func setError(_ newValue: ImageLoadingError?) { error = newValue }\n    \n    private let autoBypassImageProxy: Bool\n    private var proxyBypass: URL?\n    \n    private let size: CGSize?\n    private let processors: [any ImageProcessing]\n    \n    public init(url: URL? = nil, size: CGSize? = nil, autoBypassImageProxy: Bool) {\n        self.url = url\n        self.size = size\n        self.autoBypassImageProxy = autoBypassImageProxy\n        \n        if let size {\n            self.processors = [.resize(size: size)]\n        } else {\n            self.processors = .init()\n        }\n        \n        self.proxyBypass = computeProxyBypass(for: url)\n        \n        if let cachedImage = retrieveCachedImage(for: url, with: processors) {\n            self.mediaType = cachedImage\n            self.loading = .done\n            return\n        }\n        \n        self.mediaType = nil\n        self.loading = url == nil ? .failed : .loading\n    }\n    \n    /// Loads the given url.\n    public func load(_ url: URL?) async {\n        // noop if url unchanged and loading done\n        guard !(url == self.url && loading == .done) else {\n            return\n        }\n        \n        // reset everything\n        await setUrl(url)\n        await setMediaType(nil)\n        await setLoading(.loading)\n        await setError(nil)\n        \n        proxyBypass = computeProxyBypass(for: url)\n        \n        // easy case: nil url\n        guard let url else {\n            await setLoading(.failed)\n            return\n        }\n        \n        // handle previews\n        #if DEBUG\n            if url.scheme == \"mlempreview\" {\n                await setMediaType(.image(.init(named: url.lastPathComponent)!))\n                await setLoading(.done)\n                return\n            }\n        #endif\n        \n        // if already in cache, take the cached value\n        if let mediaType = retrieveCachedImage(for: url, with: processors) {\n            await setMediaType(mediaType)\n            await setLoading(.done)\n            return\n        }\n        \n        // otherwise actually load the image\n        do {\n            let imageTask = ImagePipeline.shared.imageTask(with: .init(\n                urlRequest: mlemUrlRequest(url: url),\n                processors: processors\n            ))\n            imageTask.priority = .veryHigh\n            \n            let container = try await imageTask.response.container\n            \n            await setMediaType(container.animatedMediaType)\n            await setLoading(.done)\n            return\n        } catch {\n            if let proxyBypass, autoBypassImageProxy {\n                await load(proxyBypass)\n            } else {\n                if let proxyBypass {\n                    await setError(.proxyFailure(proxyBypass: proxyBypass))\n                    await setLoading(.proxyFailed)\n                } else {\n                    await setError(.error(error: error))\n                    await setLoading(.failed)\n                }\n            }\n        }\n    }\n}\n\n// MARK: Helpers\n\nfunc retrieveCachedImage(for url: URL?, with processors: [ImageProcessing]) -> MediaType? {\n    if let url,\n       let container = ImagePipeline.shared.cache.cachedImage(for: .init(\n           urlRequest: mlemUrlRequest(url: url),\n           processors: processors\n       )) {\n        return container.animatedMediaType\n    }\n    return nil\n}\n\nfunc computeProxyBypass(for url: URL?) -> URL? {\n    if let url,\n       let components = URLComponents(url: url, resolvingAgainstBaseURL: false),\n       let base = components.queryItems?.first(where: { $0.name == \"url\" })?.value {\n        return .init(string: base)\n    }\n    return nil\n}\n\nextension ImageContainer {\n    var animatedMediaType: MediaType {\n        switch type {\n        case .gif, .webp:\n            if let data {\n                .animated(still: image, animated: data)\n            } else {\n                .image(image)\n            }\n        case .m4v, .mov, .mp4:\n            if let asset = userInfo[.videoAssetKey] as? AVAsset {\n                .video(still: image, animated: asset)\n            } else {\n                .image(image)\n            }\n        default:\n            .image(image)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/Sources/Media/Resources/Decoders/MlemVideoDecoder.swift",
    "content": "//\n//  MlemVideoDecoder.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2025-03-09.\n//\n//  Source: https://github.com/kean/Nuke/issues/811\n//  Wraps NukeVideo's decoder to ensure a thumbnail is always generated\n\nimport Foundation\nimport Nuke\n\npublic class MlemVideoDecoder: ImageDecoding, @unchecked Sendable {\n    private let decoder: ImageDecoders.Video\n    public var isAsynchronous: Bool { decoder.isAsynchronous }\n    \n    public init?(context: ImageDecodingContext) {\n        guard let decoder = ImageDecoders.Video(context: context) else { return nil }\n        self.decoder = decoder\n    }\n    \n    public func decode(_ data: Data) throws -> ImageContainer {\n        if let image = decoder.decodePartiallyDownloadedData(data) { return image }\n        return try decoder.decode(data)\n    }\n    \n    public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? {\n        decoder.decodePartiallyDownloadedData(data)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/Sources/Media/Resources/Decoders/NukeWebpBridgeDecoder.swift",
    "content": "//\n//  NukeWebpBridgeDecoder.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-09-24.\n//\n\nimport Foundation\nimport Nuke\nimport SDWebImageWebPCoder\nimport UIKit\n\n/// Custom Nuke decoder that processes the image using SDWebImage. The resulting ImageContainer will have the following properties:\n/// `image`: the first frame of the decoded webp\n/// `type`: `.webp`\n/// `data`: the raw webp data if the webp is animated, nil otherwise\npublic struct NukeWebpBridgeDecoder: ImageDecoding {\n    public init?(context: ImageDecodingContext) {\n        guard\n            let type = AssetType(context.data),\n            type == .webp,\n            context.data.isAnimatedWebp() // only use this for animated webp, fall back on Nuke default for non-animated\n        else { return nil }\n    }\n    \n    public func decode(_ data: Data) throws -> ImageContainer {\n        // decode the first frame to use as thumbnail\n        let decoded = SDImageWebPCoder().decodedImage(with: data, options: [.decodeFirstFrameOnly: true])\n        \n        if let ret = decoded?.cgImage {\n            return .init(image: .init(cgImage: ret), type: .webp, data: data)\n        } else {\n            return .init(image: .init())\n        }\n    }\n}\n\n/// Raw values of \"ANIM\", which we can use to identify whether a webp is animated or not without decoding it\n/// https://stackoverflow.com/questions/45190469/how-to-identify-whether-webp-image-is-static-or-animated\nprivate let animHeader: Data = .init([65, 78, 73, 77])\n\nprivate extension Data {\n    /// If the given data is a webp, returns true if that webp is animated and false otherwise.\n    /// - Warning: This function's behavior is undefined if the provided data is not a webp\n    func isAnimatedWebp() -> Bool {\n        // This function is built to run fast, banking on the fact that it's being passed in after Nuke checks that\n        // the image is a webp to guarantee safety. The check itself therefore only targets the bytes that, if the\n        // data is a webp, indicate an animated webp; it is assumed that the data is long enough and correctly formatted.\n        \n        // Sanity checks that the data conforms to the webp spec\n        assert(count >= 33, \"Invalid data (too short)\")\n        assert(self[..<4] == Data([82, 73, 70, 70]), \"Invalid data (no RIFF header)\")\n        assert(self[8 ..< 12] == Data([87, 69, 66, 80]), \"Invalid data (no WEBP header)\")\n        assert(self[12 ..< 15] == Data([86, 80, 56]), \"Invalid data (no VP8X header)\")\n        \n        return self[30 ..< 34] == animHeader\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/Sources/Media/Resources/Extensions/AVPlayer+Extensions.swift",
    "content": "//\n//  AVPlayer+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-12-09.\n//\n//  From https://stackoverflow.com/questions/11704322/how-to-check-if-avplayer-has-video-or-just-audio\n\nimport AVFoundation\n\nextension AVPlayer {\n    func isAudioAvailable() async throws -> Bool? {\n        try await currentItem?.asset.loadTracks(withMediaType: .audio).count != 0\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Media/Sources/Media/Resources/Extensions/CGSize+Extensions.swift",
    "content": "//\n//  CGSize+Extensions.swift\n//  Media\n//\n//  Created by Eric Andrews on 2025-04-20.\n//\n\nimport Foundation\n\nextension CGSize {\n    var aspectRatio: Double {\n        height / width\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/.gitignore",
    "content": ".DS_Store\n/.build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Package.resolved",
    "content": "{\n  \"originHash\" : \"433eb46e437fba071b47444bcf712c57872e1973de4812d146c7db18e50834e2\",\n  \"pins\" : [\n    {\n      \"identity\" : \"libwebp-xcode\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/libwebp-Xcode.git\",\n      \"state\" : {\n        \"revision\" : \"0d60654eeefd5d7d2bef3835804892c40225e8b2\",\n        \"version\" : \"1.5.0\"\n      }\n    },\n    {\n      \"identity\" : \"nuke\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/kean/Nuke.git\",\n      \"state\" : {\n        \"revision\" : \"0ead44350d2737db384908569c012fe67c421e4d\",\n        \"version\" : \"12.8.0\"\n      }\n    },\n    {\n      \"identity\" : \"sdwebimage\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/SDWebImage.git\",\n      \"state\" : {\n        \"revision\" : \"cac9a55a3ae92478a2c95042dcc8d9695d2129ca\",\n        \"version\" : \"5.21.0\"\n      }\n    },\n    {\n      \"identity\" : \"sdwebimagewebpcoder\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/SDWebImageWebPCoder\",\n      \"state\" : {\n        \"revision\" : \"f534cfe830a7807ecc3d0332127a502426cfa067\",\n        \"version\" : \"0.14.6\"\n      }\n    }\n  ],\n  \"version\" : 3\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"MlemBackend\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"MlemBackend\",\n            targets: [\"MlemBackend\"]\n        )\n    ],\n    dependencies: [\n        .package(path: \"../MlemLogger\"),\n        .package(path: \"../Rest\")\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"MlemBackend\",\n            dependencies: [\n                .byName(name: \"Rest\"),\n                .byName(name: \"MlemLogger\")\n            ],\n            swiftSettings: [\n                .swiftLanguageMode(.v5),\n                .enableUpcomingFeature(\"FullTypedThrows\"),\n                .enableUpcomingFeature(\"BareSlashRegexLiterals\")\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/BackendClient+Requests.swift",
    "content": "//\n//  BackendClient+Requests.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-19.\n//\n\nimport Foundation\nimport os\nimport Rest\nimport SwiftUI\nimport MlemLogger\n\nextension BackendClient {\n    public func healthCheck() async throws -> BackendHealthCheck {\n        try await perform(BackendHealthCheckRequest())\n    }\n    \n    public func getInstances() async throws -> [InstanceSummary] {\n        try await perform(BackendListInstancesRequest(\n            minTotalUsers: 20,\n            minMonthlyUsers: 1\n        ))\n    }\n\n    internal func fetchTestflightUpdate() async throws {\n        let response = try await perform(BackendGetTestflightUpdateRequest())\n        self.testflightUpdate = response.url\n    }\n    \n    internal func fetchFlairs(enabledOnly: Bool = true) async throws {\n        let response = try await perform(BackendListFlairsRequest(enabledOnly: enabledOnly))\n        \n        self.flairs = .init(developers: .init(\n            response\n                .filter { [.activeDev, .inactiveDev].contains($0.flairType) }\n                .map(\\.apId)\n        ))\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/BackendClient.swift",
    "content": "//\n//  BackendClient.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-05-31.\n//\n\nimport Foundation\nimport os\nimport Rest\nimport SwiftUI\nimport MlemLogger\n\npublic enum BackendEnvironment {\n    case qualityControl, production\n    \n    var address: URL {\n        switch self {\n        case .production: .init(string: \"https://backend.mlemapp.org:8443/\")!\n        case .qualityControl: .init(string: \"https://backend.mlemapp.org:2096/\")!\n        }\n    }\n}\n\n@Observable\npublic class BackendClient {\n    let log: Logger = .mlemLogger()\n\n    internal let restClient = RestClient(convertParamsToSnakeCase: false, decoder: .backendDecoder)\n    \n    public internal(set) var environment: BackendEnvironment = .production\n    \n    public internal(set) var flairs: MlemFlairs = .init(developers: .init())\n    public internal(set) var testflightUpdate: URL?\n    \n    internal var baseUrl: URL { environment.address }\n    \n    public init() {\n        refresh()\n    }\n    \n    public func changeEnvironment(to environment: BackendEnvironment) {\n        self.environment = environment\n        refresh()\n    }\n\n    @discardableResult\n    internal func perform<Request: RestRequest>(_ request: Request) async throws -> Request.Response {\n        return try await restClient.perform(baseUrl: baseUrl, request, token: nil)\n    }\n    \n    internal func refresh() {\n        Task {\n            do {\n                try await fetchFlairs()\n                try await fetchTestflightUpdate()\n            } catch {\n                log.error(\"\\(error.localizedDescription)\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/BackendClientError.swift",
    "content": "//\n//  BackendClientError.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-05-31.\n//\n\nenum BackendClientError: Error {\n    case malformedUrl\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/BackendHealthCheck.swift",
    "content": "//\n//  BackendHealthCheck.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-14.\n//\n\nimport Foundation\n\npublic struct BackendHealthCheck: Decodable {\n    var dbConnection: Bool\n    var lastInstanceFetch: Date\n    \n    public var unhealthyReasons: [String] {\n        guard let minimumAllowableFetch = Calendar.current.date(byAdding: .day, value: -1, to: Date()) else {\n            assertionFailure(\"Could not compute minimum allowable fetch\")\n            return [\"Could not compute minimum allowable fetch\"]\n        }\n        \n        var ret: [String] = .init()\n        \n        if !dbConnection {\n            ret.append(\"No database connection\")\n        }\n        \n        if lastInstanceFetch <= minimumAllowableFetch {\n            ret.append(\"Last fetch was \\(lastInstanceFetch.formatted(date: .abbreviated, time: .standard))\")\n        }\n        \n        return ret\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/InstanceSummary.swift",
    "content": "//\n//  InstanceSummary.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-06-01.\n//\n\nimport Foundation\n\n// The specification defined in https://github.com/mlemgroup/mlem-backend\npublic struct InstanceSummary: Codable, Hashable, Identifiable {\n    public let displayName: String\n    public let name: String\n    public let totalUsers: Int\n    public let avatar: URL?\n    public let software: InstanceSummarySoftware\n    \n    public init(\n        displayName: String,\n        name: String,\n        totalUsers: Int,\n        avatar: URL? = nil,\n        software: InstanceSummarySoftware\n    ) {\n        self.displayName = displayName\n        self.name = name\n        self.totalUsers = totalUsers\n        self.avatar = avatar\n        self.software = software\n    }\n    \n    enum CodingKeys: String, CodingKey {\n        case displayName = \"name\"\n        case name = \"host\"\n        case userCount // Removed in Mlem 2.4\n        case totalUsers\n        case avatar\n        case software\n    }\n\n    public var host: String { name }\n    public var url: URL? { URL(string: \"https://\\(host)/\") }\n    \n    public var id: String { host }\n    \n    public init(from decoder: any Decoder) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        self.displayName = try container.decode(String.self, forKey: .displayName)\n        self.name = try container.decode(String.self, forKey: .name)\n        \n        if let totalUsers = try container.decodeIfPresent(Int.self, forKey: .totalUsers) {\n            self.totalUsers = totalUsers\n        } else if let totalUsers = try container.decodeIfPresent(Int.self, forKey: .userCount) {\n            self.totalUsers = totalUsers\n        } else {\n            throw DecodingError.dataCorruptedError(forKey: .totalUsers, in: container, debugDescription: \"\")\n        }\n        \n        self.avatar = try container.decode(URL?.self, forKey: .avatar)\n        self.software = try container.decode(InstanceSummarySoftware.self, forKey: .software)\n    }\n    \n    public func encode(to encoder: any Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(displayName, forKey: .displayName)\n        try container.encode(name, forKey: .name)\n        try container.encode(totalUsers, forKey: .totalUsers)\n        try container.encode(avatar, forKey: .avatar)\n        try container.encode(software, forKey: .software)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/InstanceSummarySoftware.swift",
    "content": "//\n//  File.swift\n//  MlemBackend\n//\n//  Created by Sjmarf on 2026-03-18.\n//  \n\nimport Foundation\n\npublic struct InstanceSummarySoftware: Codable, Hashable {\n    public let type: InstanceSummarySoftwareType\n    public let version: String\n    \n    public init(type: InstanceSummarySoftwareType, version: String) {\n        self.type = type\n        self.version = version\n    }\n}\n\npublic enum InstanceSummarySoftwareType: String, Codable, Hashable {\n    case lemmy, pieFed\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/JSONDecoder+Extensions.swift",
    "content": "//\n//  JSONDecoder+Extensions.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-19.\n//\n\nimport Foundation\n\nextension JSONDecoder {\n    internal static let backendDecoder: JSONDecoder = {\n        let decoder: JSONDecoder = .init()\n        decoder.dateDecodingStrategy = .custom { decoder in\n            let formatter: ISO8601DateFormatter = .init()\n            formatter.formatOptions = [.withFractionalSeconds, .withInternetDateTime]\n            let dateStr = try decoder.singleValueContainer().decode(String.self)\n            if let date = formatter.date(from: dateStr) {\n                return date\n            }\n            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: \"Invalid date\"))\n        }\n        return decoder\n    }()\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/MlemDeveloper.swift",
    "content": "//\n//  MlemDeveloper.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-05-31.\n//\n\npublic struct MlemDeveloper: Codable {\n    public let apId: String\n    public let active: Bool\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/MlemFlairs.swift",
    "content": "//\n//  Flairs.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-06-05.\n//\n\npublic enum MlemFlairType: String, Codable {\n    case activeDev, inactiveDev\n}\n\nstruct MlemFlair: Codable {\n    let apId: String\n    let flairType: MlemFlairType\n    let flairEnabled: Bool\n}\n\npublic struct MlemFlairs {\n    /// apIds of users who should have the Developer flair\n    public let developers: Set<String>\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/Requests/BackendGetTestflightUpdateRequest.swift",
    "content": "//\n//  BackendGetTestflightUpdateRequest.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-19.\n//\n\nimport Rest\n\ninternal struct BackendGetTestflightUpdateRequest: GetRequest {\n    typealias Parameters = Never\n    typealias Response = TestflightUpdate\n    \n    let path: String = \"v0/mlem/testflight\"\n    let parameters: Parameters? = nil\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/Requests/BackendHealthCheckRequest.swift",
    "content": "//\n//  BackendHealthCheckRequest.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-19.\n//\n\nimport Rest\n\ninternal struct BackendHealthCheckRequest: GetRequest {\n    typealias Parameters = Never\n    typealias Response = BackendHealthCheck\n    \n    let path: String = \"v0/health\"\n    let parameters: Parameters? = nil\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/Requests/BackendListFlairsRequest.swift",
    "content": "//\n//  BackendListFlairsRequest.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-19.\n//\n\nimport Foundation\nimport Rest\n\ninternal struct BackendListFlairsRequest: GetRequest {\n    struct Parameters: Encodable {\n        let enabledOnly: Bool\n    }\n\n    typealias Response = [MlemFlair]\n    \n    let path: String = \"v0/mlem/flairs\"\n    var parameters: Parameters?\n\n    init(enabledOnly: Bool) {\n        self.parameters = .init(enabledOnly: enabledOnly)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/Requests/BackendListInstancesRequest.swift",
    "content": "//\n//  BackendListInstancesRequest.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-19.\n//\n\nimport Foundation\nimport Rest\n\ninternal struct BackendListInstancesRequest: GetRequest {\n    struct Parameters: Encodable {\n        let minTotalUsers: Int\n        let minMonthlyUsers: Int\n    }\n\n    typealias Response = [InstanceSummary]\n    \n    let path: String = \"v1/stats/instances\"\n    var parameters: Parameters?\n\n    init(minTotalUsers: Int, minMonthlyUsers: Int) {\n        self.parameters = .init(\n            minTotalUsers: minTotalUsers,\n            minMonthlyUsers: minMonthlyUsers\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemBackend/Sources/MlemBackend/TestflightUpdate.swift",
    "content": "//\n//  TestflightUpdate.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-06-02.\n//\n\nimport Foundation\n\nstruct TestflightUpdate: Codable {\n    let updatedAt: Date\n    let url: URL\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemLogger/.gitignore",
    "content": ".DS_Store\n/.build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\n"
  },
  {
    "path": "Mlem/Packages/MlemLogger/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"MlemLogger\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"MlemLogger\",\n            targets: [\"MlemLogger\"]\n        )\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"MlemLogger\"\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/MlemLogger/Sources/MlemLogger/MlemLogger.swift",
    "content": "//\n//  MlemLogger.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-10-24.\n//\n\nimport os\n\npublic extension Logger {\n    static func mlemLogger(file: String = #file) -> Logger {\n        let splitFile = file.split(separator: \"/\")\n        return Logger(subsystem: String(splitFile.first ?? \"Unknown\"), category: String(splitFile.last ?? \"Unknown\"))\n    }\n    \n    /// Singleton logger to be used ONLY where access to a relevant specific logger is not available. Use of\n    /// this logger is discouraged except where absolutely necessary. Ensure any messages sent to this logger\n    /// contain enough contextual information to determine their source.\n    static let universal: Logger = .init(subsystem: \"Universal\", category: \"Logger\")\n    \n    #if DEBUG\n        /// Singleton logger for temporary development logs.\n        /// - Warning: by design, release builds will fail if any messages to this logger are present\n        static let dev: Logger = .init(subsystem: \"Dev\", category: \"Logger\")\n    #endif\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/.gitignore",
    "content": ".DS_Store\n/.build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/CODEOWNERS",
    "content": "*   @mlemgroup/mlem-dev-team\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <https://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    <program>  Copyright (C) <year>  <name of author>\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<https://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<https://www.gnu.org/licenses/why-not-lgpl.html>.\n\n----\n\nThe MlemMiddleware iOS developers are aware that the terms of service that apply to apps distributed via Apple's App Store services may conflict with rights granted under\nthe MlemMiddleware iOS license, the \"GNU GPLv3\". We have committed not to pursue any license violation that results solely from the conflict between\nthe \"GNU GPLv3\" or any later version and the Apple App Store terms of service.\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/NOTICE.md",
    "content": "# NOTICE\n\nThis project makes use of several other open source projects; their names and licenses are listed below. We thank all of the developers for their hard work.\n\n## Semaphore\n\nhttps://github.com/groue/Semaphore\n\n```\nMIT License\n\nCopyright (c) 2022 Gwendal Roué\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n\n## Nuke\n\nhttps://github.com/kean/Nuke\n\n```\nThe MIT License (MIT)\n\nCopyright (c) 2015-2024 Alexander Grebenyuk\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n\n## ReactiveSwift\n\nhttps://github.com/ReactiveCocoa/ReactiveSwift\n\n```\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n```\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Package.resolved",
    "content": "{\n  \"originHash\" : \"9aa28d6a6e9458feceedf48c99d995409fde77733c5c4421fc7c67a330763bb6\",\n  \"pins\" : [\n    {\n      \"identity\" : \"collectionconcurrencykit\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/JohnSundell/CollectionConcurrencyKit.git\",\n      \"state\" : {\n        \"revision\" : \"b4f23e24b5a1bff301efc5e70871083ca029ff95\",\n        \"version\" : \"0.2.0\"\n      }\n    },\n    {\n      \"identity\" : \"nuke\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/kean/Nuke\",\n      \"state\" : {\n        \"revision\" : \"8e431251dea0081b6ab154dab61a6ec74e4b6577\",\n        \"version\" : \"12.6.0\"\n      }\n    },\n    {\n      \"identity\" : \"semaphore\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/groue/Semaphore\",\n      \"state\" : {\n        \"revision\" : \"f1c4a0acabeb591068dea6cffdd39660b86dec28\",\n        \"version\" : \"0.0.8\"\n      }\n    }\n  ],\n  \"version\" : 3\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"MlemMiddleware\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"MlemMiddleware\",\n            targets: [\"MlemMiddleware\"]\n        )\n    ],\n    dependencies: [\n        .package(url: \"https://github.com/groue/Semaphore.git\", .upToNextMajor(from: \"0.0.8\")),\n        .package(url: \"https://github.com/kean/Nuke.git\", .upToNextMajor(from: \"12.6.0\")),\n        .package(url: \"https://github.com/JohnSundell/CollectionConcurrencyKit.git\", .upToNextMajor(from: \"0.2.0\")),\n        .package(path: \"../Rest\"),\n        .package(path: \"../MlemBackend\")\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"MlemMiddleware\",\n            dependencies: [\n                .product(name: \"Semaphore\", package: \"Semaphore\"),\n                .product(name: \"Nuke\", package: \"Nuke\"),\n                .product(name: \"CollectionConcurrencyKit\", package: \"CollectionConcurrencyKit\"),\n                .byName(name: \"Rest\"),\n                .byName(name: \"MlemBackend\")\n            ],\n            swiftSettings: [\n                .swiftLanguageMode(.v5),\n                .enableUpcomingFeature(\"BareSlashRegexLiterals\")\n            ]\n        ),\n        .testTarget(\n            name: \"MlemMiddlewareTests\",\n            dependencies: [\"MlemMiddleware\"]\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+Caches.swift",
    "content": "//\n//  ApiClient+Caches.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-01.\n//\n\nimport Foundation\n\npublic struct WeakReference<Content: AnyObject> {\n    public weak var content: Content?\n    \n    public init(content: Content) {\n        self.content = content\n    }\n}\n\npublic protocol CacheIdentifiable {\n    var cacheId: Int { get }\n}\n\nextension ApiClient {\n    struct BaseCacheGroup {\n        var instance: InstanceCache = .init()\n        \n        var community: CommunityCache = .init()\n        var person: PersonCache = .init()\n        var post: PostCache = .init()\n        var comment: CommentCache = .init()\n        \n        var message1: Message1Cache = .init()\n        var message2: Message2Cache = .init()\n        \n        var imageUpload1: ImageUpload1Cache = .init()\n        \n        var report: ReportCache = .init()\n        \n        var personVote: PersonVoteCache = .init()\n        \n        var registrationApplication: RegistrationApplicationCache = .init()\n\n        var notification: NotificationCache = .init()\n        \n        func clean() {\n            instance.clean()\n            community.clean()\n            person.clean()\n            post.clean()\n            comment.clean()\n            message1.clean()\n            message2.clean()\n            imageUpload1.clean()\n            report.clean()\n            personVote.clean()\n            registrationApplication.clean()\n            notification.clean()\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+Comment.swift",
    "content": "//\n//  ApiClient+Comment.swift\n//\n//\n//  Created by Sjmarf on 24/06/2024.\n//\n\nimport Foundation\n\npublic extension ApiClient {\n    func getComment(id: Int) async throws -> Comment {\n        let snapshot = try await repository.getComment(id: id)\n        return await caches.comment.getModel(api: self, from: .comment2(snapshot))\n    }\n    \n    func getComment(url: URL) async throws -> Comment {\n        let snapshot = try await repository.getComment(url: url)\n        return await caches.comment.getModel(api: self, from: .comment2(snapshot))\n    }\n    \n    func getComments(\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment] {\n        let snapshots = try await repository.getComments(\n            sort: sort,\n            page: page,\n            maxDepth: maxDepth,\n            limit: limit,\n            filter: filter\n        )\n        return await caches.comment.getModels(api: self, from: snapshots.map { .comment2($0) })\n    }\n    \n    func getComments(\n        postId: Int,\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment] {\n        let snapshots = try await repository.getComments(\n            postId: postId,\n            sort: sort,\n            page: page,\n            maxDepth: maxDepth,\n            limit: limit,\n            filter: filter\n        )\n        return await caches.comment.getModels(api: self, from: snapshots.map { .comment2($0) })\n    }\n    \n    func getComments(\n        parentId: Int,\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment] {\n        let snapshots = try await repository.getComments(\n            parentId: parentId,\n            sort: sort,\n            page: page,\n            maxDepth: maxDepth,\n            limit: limit,\n            filter: filter\n        )\n        return await caches.comment.getModels(api: self, from: snapshots.map { .comment2($0) })\n    }\n\n    func getCommentHistory(\n        type: GetContentFilter,\n        page: Int?,\n        cursor: String?,\n        limit: Int\n    ) async throws -> (comments: [Comment], cursor: String?) {\n        let response = try await repository.getCommentHistory(\n            type: type,\n            page: page,\n            cursor: cursor,\n            limit: limit\n        )\n        return await (\n            comments: caches.comment.getModels(api: self, from: response.comments.map { .comment2($0) }),\n            cursor: response.cursor\n        )\n    }\n    \n    // TODO: Remove in favor of the below method once we drop support for versions before Lemmy 1.0\n    func searchComments(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: CommentSortType = .top(.allTime)\n    ) async throws -> [Comment] {\n        let snapshots = try await repository.searchComments(\n            query: query,\n            page: page,\n            limit: limit,\n            communityId: communityId,\n            creatorId: creatorId,\n            filter: filter,\n            sort: sort\n        )\n        return await caches.comment.getModels(api: self, from: snapshots.map { .comment2($0) })\n    }\n    \n    func searchComments(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) async throws -> [Comment] {\n        let snapshots = try await repository.searchComments(\n            query: query,\n            page: page,\n            limit: limit,\n            communityId: communityId,\n            creatorId: creatorId,\n            filter: filter,\n            sort: sort\n        )\n        return await caches.comment.getModels(api: self, from: snapshots.map { .comment2($0) })\n    }\n    \n    // TODO: UpdateQueue remove (currently needed for Reply)\n    @discardableResult\n    func voteOnComment(id: Int, score: ScoringOperation, semaphore: UInt? = nil) async throws -> Comment {\n        let snapshot = try await repository.voteOnComment(id: id, score: score)\n        return await caches.comment.getModel(\n            api: self,\n            from: .comment2(snapshot),\n            semaphore: semaphore\n        )\n    }\n    \n    // TODO: UpdateQueue remove (currently needed for Reply)\n    @discardableResult\n    func saveComment(id: Int, save: Bool, semaphore: UInt? = nil) async throws -> Comment {\n        let snapshot = try await repository.saveComment(id: id, save: save)\n        return await caches.comment.getModel(\n            api: self,\n            from: .comment2(snapshot),\n            semaphore: semaphore\n        )\n    }\n    \n    // There's also a `replyToPost` method in `ApiClient+Post` for creating a comment on a post\n    func replyToComment(postId: Int, parentId: Int?, content: String, languageId: Int? = nil) async throws -> Comment {\n        let snapshot = try await repository.replyToComment(\n            postId: postId,\n            parentId: parentId,\n            content: content,\n            languageId: languageId\n        )\n        return await caches.comment.getModel(api: self, from: .comment2(snapshot))\n    }\n    \n    @discardableResult\n    func reportComment(id: Int, reason: String) async throws -> Report {\n        let snapshot = try await repository.reportComment(id: id, reason: reason)\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.report.getModel(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId\n        )\n    }\n    \n    func purgeComment(id: Int, reason: String?) async throws {\n        try await repository.purgeComment(id: id, reason: reason)\n    }\n    \n    @discardableResult\n    func getCommentVotes(\n        id: Int,\n        communityId: Int,\n        page: Int = 1,\n        limit: Int = 20\n    ) async throws -> [PersonVote] {\n        let snapshot = try await repository.getCommentVotes(\n            id: id,\n            page: page,\n            limit: limit\n        )\n        return await caches.personVote.getModels(\n            api: self,\n            from: snapshot,\n            target: .comment(id: id),\n            communityId: communityId\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+Community.swift",
    "content": "//\n//  NewApiClient+Requests.swift\n//  Mlem\n//\n//  Created by Sjmarf on 10/02/2024.\n//\n\nimport Foundation\n\npublic extension ApiClient {\n    func decodeCommunity(_ data: Community.CodedData) async throws -> Community {\n        guard data.apiUrl == baseUrl else {\n            throw ApiClientError.mismatchingUrl\n        }\n        guard try await data.apiMyPersonId == myPersonId else {\n            throw ApiClientError.mismatchingPersonId\n        }\n        return try await caches.community.getModel(\n            api: self,\n            from: .community1(.init(from: data.apiCommunity)),\n            isStale: true\n        )\n    }\n    \n    func getCommunity(id: Int) async throws -> Community {\n        let snapshot = try await repository.getCommunity(id: id)\n        return await caches.community.getModel(api: self, from: .community3(snapshot))\n    }\n    \n    func getCommunity(url: URL) async throws -> Community {\n        let snapshot: Community2Snapshot = try await repository.getCommunity(url: url)\n        return await caches.community.getModel(api: self, from: .community2(snapshot))\n    }\n    \n    func searchCommunities(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        filter: ListingType = .all,\n        sort sort_: SearchSortType? = nil,\n        hostApi: ApiClient? = nil\n    ) async throws -> [Community] {\n        let sort: SearchSortType\n        if let sort_ {\n            sort = sort_\n        } else if try await software.supports(.searchSortType(.top(.allTime))) {\n            sort = .top(.allTime)\n        } else {\n            sort = .top(.limited(.month))\n        }\n        \n        let snapshots = try await repository.searchCommunities(\n            query: query,\n            page: page,\n            limit: limit,\n            filter: filter,\n            sort: sort\n        )\n        \n        let ret = await caches.community.getModels(api: self, from: snapshots.map { .community2($0) })\n        if let subscriptionInfo = hostApi?.subscriptions {\n            for community in ret {\n                if let subscribedCommunity = subscriptionInfo.communities.first(where: { $0.actorId == community.actorId }) {\n                    community.subscription.addSibling(subscribedCommunity.subscription)\n                }\n                // TODO: favorites\n            }\n        }\n        \n        // if on a foreign host, resolve communities to populate subscription status.\n        if let hostApi, hostApi !== self {\n            do {\n                let resolvedCommunities: [URL: Community] = try await hostApi.resolve(urls: ret.map { $0.resolvableUrl(from: .host) })\n                for community in ret {\n                    if let resolvedCommunity = resolvedCommunities[community.resolvableUrl(from: .host)] {\n                        community.blocked_.addSibling(resolvedCommunity.blocked_)\n                    }\n                }\n            } catch {\n                // if this fails, don't fail the whole call\n                // TODO: error toast (depends on packaged error handling)\n                log.error(\"Failed to resolve community URLs: \\(error)\")\n            }\n        }\n        return ret\n    }\n    \n    func setupSubscriptionList(\n        getFavorites: @escaping () -> Set<Int> = { [] },\n        setFavorites: @escaping (Set<Int>) -> Void = { _ in }\n    ) -> SubscriptionList {\n        if let subscriptions {\n            return subscriptions\n        } else {\n            let new: SubscriptionList = .init(apiClient: self, getFavorites: getFavorites, setFavorites: setFavorites)\n            subscriptions = new\n            return new\n        }\n    }\n    \n    @discardableResult\n    func getSubscriptionList() async throws -> SubscriptionList {\n        let subscriptionList = setupSubscriptionList()\n        \n        let limit = 50\n        var page = 1\n        var hasMorePages = true\n        var communities = [Community2Snapshot]()\n        \n        repeat {\n            let snapshots = try await repository.getSubscriptionList(page: page, limit: limit)\n            communities.append(contentsOf: snapshots)\n            hasMorePages = snapshots.count >= limit\n            page += 1\n        } while hasMorePages\n            \n        let models: Set<Community> = await Set(caches.community.getModels(api: self, from: communities.map { .community2($0) }))\n        await subscriptionList.updateCommunities(with: models)\n        subscriptionList.hasLoaded = true\n        return subscriptionList\n    }\n    \n    func purgeCommunity(id: Int, reason: String?) async throws {\n        try await repository.purgeCommunity(id: id, reason: reason)\n        caches.community.retrieveModel(cacheId: id)?.purged = true\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+General.swift",
    "content": "//\n//  ApiClient+General.swift\n//\n//\n//  Created by Sjmarf on 25/06/2024.\n//\n\nimport Foundation\n\npublic extension ApiClient {\n    var isAdmin: Bool {\n        myInstance?.administrators.value?.contains(where: { $0.id == myPerson?.id }) ?? false\n    }\n    \n    /// Returns true if both myPerson and the given person are admins on this instance and myPerson outranks the given person, false otherwise\n    func isHigherAdmin(than person: Person) -> Bool {\n        guard person.api.actorId == actorId,\n              let myPerson,\n              let myAdminIndex = myInstance?.administrators.value?.firstIndex(of: myPerson),\n              let targetAdminIndex = myInstance?.administrators.value?.firstIndex(where: { $0.actorId == person.actorId }) else {\n            return false\n        }\n        return myAdminIndex < targetAdminIndex\n    }\n    \n    func getAccountToken(usernameOrEmail: String, password: String, totpToken: String?) async throws -> String {\n        try await repository.getAccountToken(usernameOrEmail: usernameOrEmail, password: password, totpToken: totpToken)\n    }\n    \n    func getUsernameFromToken(token: String) async throws -> String {\n        try await repository.getUsernameFromToken(token: token)\n    }\n    \n    func login(password: String, totpToken: String?) async throws {\n        guard let username else { throw ApiClientError.notLoggedIn }\n        let token = try await getAccountToken(usernameOrEmail: username, password: password, totpToken: totpToken)\n        updateToken(token)\n    }\n    \n    func signUp(\n        username: String,\n        password: String,\n        confirmPassword: String,\n        showNsfw: Bool,\n        email: String?,\n        captcha: Captcha?,\n        captchaAnswer: String?,\n        applicationQuestionResponse: String?\n    ) async throws -> SignUpResponse {\n        try await repository.signUp(username: username, password: password, confirmPassword: confirmPassword, showNsfw: showNsfw, email: email, captcha: captcha, captchaAnswer: captchaAnswer, applicationQuestionResponse: applicationQuestionResponse)\n    }\n    \n    @discardableResult\n    func changePassword(\n        newPassword: String,\n        confirmNewPassword: String,\n        oldPassword: String\n    ) async throws -> String {\n        let token = try await repository.changePassword(newPassword: newPassword, confirmNewPassword: confirmNewPassword, oldPassword: oldPassword)\n        updateToken(token)\n        return token\n    }\n    \n    func getCaptcha() async throws -> Captcha {\n        try await repository.getCaptcha()\n    }\n    \n    /// Returns an object associated with the given URL.\n    ///\n    /// ## Overview\n    ///\n    /// The backend performs two steps to do this:\n    /// 1) Check it already has the given actorId mapped in the database, in which case it returns the entity.\n    /// 2) If the entity is not present in the database, it contacts the URL host to ask for it, then returns it back to us.\n    ///   When this happens, the call will take longer to resolve.\n    ///\n    /// **Importantly, step 2) is only performed if the `ApiClient` is authenticated.**\n    ///\n    func resolve(url: URL) async throws -> (any ActorIdentifiable & Sharable) {\n        let response = try await repository.resolve(url: url)\n        return switch response {\n        case let .comment(comment):\n            await caches.comment.getModel(api: self, from: .comment2(comment))\n        case let .post(post):\n            await caches.post.getModel(api: self, from: .post2(post))\n        case let .community(community):\n            await caches.community.getModel(api: self, from: .community2(community))\n        case let .person(person):\n            await caches.person.getModel(api: self, from: .person2(person))\n        }\n    }\n    \n    func resolve<Value: ActorIdentifiable & Sharable>(urls: [URL]) async throws -> [URL: Value] {\n        try await withThrowingTaskGroup(of: (url: URL, value: Value)?.self) { group in\n            for url in urls {\n                group.addTask {\n                    if let value = try await self.resolve(url: url) as? Value {\n                        return (url, value)\n                    }\n                    return nil\n                }\n            }\n            \n            var collected: [URL: Value] = .init()\n            \n            for try await result in group {\n                if let result {\n                    collected[result.url] = result.value\n                }\n            }\n            \n            return collected\n        }\n    }\n    \n    func getBlocked() async throws -> (people: [Person], communities: [Community], instances: [Instance]) {\n        let snapshots = try await repository.getBlocked()\n        return await (\n            people: caches.person.getModels(api: self, from: snapshots.people.map { .person1($0) }),\n            communities: caches.community.getModels(api: self, from: snapshots.communities.map { .community1($0) }),\n            instances: caches.instance.getModels(api: self, from: snapshots.instances.map { .instance1($0) })\n        )\n    }\n    \n    func getModlog(\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        moderatorId: Int? = nil,\n        subjectPersonId: Int? = nil,\n        postId: Int? = nil,\n        commentId: Int? = nil,\n        type: ModlogEntryType? = nil\n    ) async throws -> [ModlogEntry] {\n        let snapshots = try await repository.getModlog(\n            page: page,\n            limit: limit,\n            communityId: communityId,\n            moderatorId: moderatorId,\n            subjectPersonId: subjectPersonId,\n            postId: postId,\n            commentId: commentId,\n            type: type\n        )\n        return await createModlogEntries(snapshots)\n    }\n    \n    @MainActor\n    private func createModlogEntries(_ entries: [ModlogEntrySnapshot]) -> [ModlogEntry] {\n        entries.map { entry in\n            return ModlogEntry(\n                api: self,\n                created: entry.created,\n                moderator: caches.person.getOptionalModel(\n                    api: self,\n                    from: .person1(entry.moderator)\n                ),\n                type: .init(from: entry.type, api: self)\n            )\n        }\n    }\n    \n    func getPostLink(url: URL) async throws -> PostLink {\n        try await repository.getPostLink(url: url)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+Image.swift",
    "content": "//\n//  ApiClient+Image.swift\n//\n//\n//  Created by Sjmarf on 26/08/2024.\n//\n\nimport Foundation\nimport Rest\n\npublic extension ApiClient {\n    func uploadImage(\n        _ imageData: Data,\n        fileExtension: String,\n        onProgress progressCallback: @escaping (_ progress: Double) -> Void = { _ in }\n    ) async throws -> ImageUpload1 {\n        let file = try await repository.uploadImage(imageData, fileExtension: fileExtension, onProgress: progressCallback)\n        return caches.imageUpload1.getModel(api: self, from: file)\n    }\n    \n    func deleteImage(alias: String, deleteToken: String) async throws {\n        try await repository.deleteImage(alias: alias, deleteToken: deleteToken)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+Inbox.swift",
    "content": "//\n//  ApiClient+Inbox.swift\n//\n//\n//  Created by Sjmarf on 04/07/2024.\n//\n\nimport Foundation\n\npublic extension ApiClient {\n    func getMessages(\n        creatorId: Int? = nil,\n        page: Int,\n        limit: Int,\n        unreadOnly: Bool = false\n    ) async throws -> [Message2] {\n        let snapshots = try await repository.getMessages(\n            creatorId: creatorId,\n            page: page,\n            limit: limit,\n            unreadOnly: unreadOnly\n        )\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.message2.getModels(\n            api: self,\n            from: snapshots,\n            myPersonId: myPersonId\n        )\n    }\n\n    func getReplyNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotification], cursor: String?) {\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        let response = try await repository.getReplyNotifications(\n            page: page,\n            cursor: cursor,\n            limit: limit,\n            unreadOnly: unreadOnly\n        )\n        return await (\n            notifications: caches.notification.getModels(api: self, from: response.notifications, myPersonId: myPersonId),\n            cursor: response.cursor\n        )\n    }\n    \n    func getMentionNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotification], cursor: String?) {\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        let response = try await repository.getMentionNotifications(\n            page: page,\n            cursor: cursor,\n            limit: limit,\n            unreadOnly: unreadOnly\n        )\n        return await (\n            notifications: caches.notification.getModels(api: self, from: response.notifications, myPersonId: myPersonId),\n            cursor: response.cursor\n        )\n    }\n\n    func getMessageNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotification], cursor: String?) {\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        let response = try await repository.getMessageNotifications(\n            page: page,\n            cursor: cursor,\n            limit: limit,\n            unreadOnly: unreadOnly\n        )\n        return await (\n            notifications: caches.notification.getModels(api: self, from: response.notifications, myPersonId: myPersonId),\n            cursor: response.cursor\n        )\n    }\n\n    func markAllAsRead() async throws {\n        try await repository.markAllAsRead()\n        _ = await Task { @MainActor in\n            for notification in caches.notification.itemCache.value.values {\n                notification.content?.read = true\n            }\n        }.result\n        unreadCount?.clear(.personal)\n    }\n    \n    /// Get an ``UnreadCount`` object that continues to be updated by the ``ApiClient`` whenever an inbox item is marked read/unread.\n    func getUnreadCount(alwaysMakeCalls: Bool = false) async throws -> UnreadCount {\n        let unreadCount = unreadCount ?? .init(api: self)\n        try await unreadCount.refresh(alwaysMakeCalls: alwaysMakeCalls)\n        self.unreadCount = unreadCount\n        return unreadCount\n    }\n    \n    func createMessage(personId: Int, content: String) async throws -> Message2 {\n        let snapshot = try await repository.createMessage(personId: personId, content: content)\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.message2.getModel(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId\n        )\n    }\n    \n    @discardableResult\n    func editMessage(id: Int, content: String) async throws -> Message2 {\n        let snapshot = try await repository.editMessage(id: id, content: content)\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.message2.getModel(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId\n        )\n    }\n    \n    @discardableResult\n    func reportMessage(id: Int, reason: String) async throws -> Report {\n        let snapshot = try await repository.reportMessage(id: id, reason: reason)\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.report.getModel(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId\n        )\n    }\n    \n    @discardableResult\n    func deleteMessage(id: Int, delete: Bool, semaphore: UInt? = nil) async throws -> Message2 {\n        let snapshot = try await repository.deleteMessage(id: id, delete: delete)\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.message2.getModel(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId,\n            semaphore: semaphore\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+Instance.swift",
    "content": "//\n//  NewApiClient+Site.swift\n//  Mlem\n//\n//  Created by Sjmarf on 12/02/2024.\n//\n\nimport Foundation\n\npublic extension ApiClient {\n    func getMyInstance() async throws -> Instance {\n        let snapshot = try await repository.getMyInstance()\n        let model = await caches.instance.getModel(api: self, from: .instance3(snapshot))\n        model.local = true\n        _ = await Task { @MainActor in\n            myInstance = model\n        }.result\n        return model\n    }\n    \n    /// Returns `true` if federated, `false` if not federated, or `nil` if the status could not be determined.\n    func federatedWith(with url: URL) async throws -> FederationStatus? {\n        guard let domain = url.host() else { throw ApiClientError.invalidInput }\n        let federatedInstances = try await repository.getFederatedInstances()\n        if !federatedInstances.blocked.isEmpty {\n            return federatedInstances.blocked.contains(domain) ? .explicitlyBlocked : .implicitlyAllowed\n        } else if !federatedInstances.allowed.isEmpty {\n            return federatedInstances.allowed.contains(domain) ? .explicitlyAllowed : .implicitlyBlocked\n        }\n        return nil\n    }\n\n    /// Get any `Community3` hosted on the given instance.\n    internal func getCommunityOfInstance(actorId: ActorIdentifier) async throws -> Community {\n        let externalApi: ApiClient = .getApiClient(url: actorId.url, username: nil)\n        \n        let response = try await externalApi.getPosts(\n            feed: .local,\n            sort: .new,\n            page: 1,\n            cursor: nil,\n            limit: 1\n        )\n        \n        guard let post = response.posts.first else {\n            throw InstanceUpgradeError.noPostReturned\n        }\n        \n        guard let community = post.community.value_ else {\n            throw InstanceUpgradeError.noCommunityReturned\n        }\n        \n        return try await self.getCommunity(url: community.actorId.url)\n    }\n\n    func getInstanceId(actorId: ActorIdentifier) async throws -> Int {\n        let comm = try await self.getCommunityOfInstance(actorId: actorId)\n        return comm.instanceId\n    }\n    \n    \n    /// `instanceId` is distinct from `id`. Make sure to pass `instance.instanceId` and not `id`.\n    ///  Technically only `instanceId` is needed to perform this request, but `actorId` is also needed to properly update the `BlockList`.\n    func blockInstance(url: URL, instanceId: Int, block: Bool, semaphore: UInt? = nil) async throws {\n        guard let host = url.host() else { throw ApiClientError.invalidInput }\n        let actorId: ActorIdentifier = .instance(host: host)\n        try await repository.blockInstance(instanceId: instanceId, block: block)\n        let newBlockState: Bool = block\n        if let instance = caches.instance.retrieveModel(instanceId: instanceId) {\n            instance.blocked_.set(newBlockState)\n        }\n        if newBlockState {\n            blocks?.instances[actorId] = instanceId\n        } else {\n            blocks?.instances.removeValue(forKey: actorId)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+Mock.swift",
    "content": "//\n//  ApiClient+Mock.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-02.\n//\n\nimport Foundation\nimport Rest\n\n// TODO: updated mocks\n//#if DEBUG\n//\n//public extension ApiClient {\n//    static let mock: MockApiClient = .init()\n//}\n//\n//public class MockApiClient: ApiClient {\n//    public init(\n//        posts: [Post2] = [],\n//        communities: [Community2] = [],\n//        people: [Person2] = [],\n//        comments: [Comment2] = []\n//    ) {\n//        let url = URL(string: \"https://lemmy.world/\")!\n//        let username = \"\"\n//        super.init(\n//            url: url,\n//            username: username\n//        )\n//        \n//        self.repository = MockApiRepository(url: url, username: username, posts: posts, communities: communities, people: people, comments: comments)\n//    }\n//    \n//    private var mockRepository: MockApiRepository { repository as! MockApiRepository }\n//    \n//    public func setPosts(_ posts: [Post2]) {\n//        mockRepository.posts = posts\n//    }\n//    \n//    public func setCommunities(_ communities: [Community2]) {\n//        mockRepository.communities = communities\n//    }\n//    \n//    public func setPeople(_ people: [Person2]) {\n//        mockRepository.people = people\n//    }\n//}\n//\n//#endif\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+Person.swift",
    "content": "//\n//  NewApiClient+User.swift\n//  Mlem\n//\n//  Created by Sjmarf on 12/02/2024.\n//\n\nimport Foundation\n\npublic extension ApiClient {\n    func decodePerson(_ data: Person.CodedData) async throws -> Person {\n        guard data.apiUrl == baseUrl else {\n            throw ApiClientError.mismatchingUrl\n        }\n        guard try await data.apiMyPersonId == myPersonId else {\n            throw ApiClientError.mismatchingPersonId\n        }\n        return try await caches.person.getModel(\n            api: self,\n            from: .person1(.init(from: data.apiPerson)),\n            isStale: true\n        )\n    }\n    \n    func getPerson(id: Int) async throws -> Person {\n        let snapshot = try await repository.getPerson(id: id)\n        return await caches.person.getModel(api: self, from: .person3(snapshot))\n    }\n    \n    func getPerson(url: URL) async throws -> Person {\n        let snapshot: Person2Snapshot = try await repository.getPerson(url: url)\n        return await caches.person.getModel(api: self, from: .person2(snapshot))\n    }\n    \n    func getPerson(username: String) async throws -> Person {\n        let snapshot: Person3Snapshot = try await repository.getPerson(username: username)\n        return await caches.person.getModel(api: self, from: .person3(snapshot))\n    }\n    \n    /// `filter` can be set to `.local` from 0.19.4 onwards.\n    func searchPeople(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        filter: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) async throws -> [Person] {\n        let snapshots = try await repository.searchPeople(\n            query: query,\n            page: page,\n            limit: limit,\n            filter: filter,\n            sort: sort\n        )\n        return await caches.person.getModels(api: self, from: snapshots.map { .person2($0) })\n    }\n    \n    @discardableResult\n    func blockPerson(id: Int, block: Bool, semaphore: UInt? = nil) async throws -> Person {\n        let snapshot = try await repository.blockPerson(id: id, block: block)\n        return await caches.person.getModel(\n            api: self,\n            from: .person2(snapshot),\n            semaphore: semaphore\n        )\n    }\n    \n    @discardableResult\n    func banPersonFromCommunity(\n        personId: Int,\n        communityId: Int,\n        ban: Bool,\n        removeContent: Bool,\n        reason: String?,\n        expires: Date? = nil\n    ) async throws -> Person {\n        let snapshot = try await repository.banPersonFromCommunity(\n            personId: personId,\n            communityId: communityId,\n            ban: ban,\n            removeContent: removeContent,\n            reason: reason,\n            expires: expires\n        )\n        let person = await caches.person.getModel(\n            api: self,\n            from: .person1(snapshot)\n        )\n        person.updateKnownCommunityBanState(id: communityId, banned: ban)\n        return person\n    }\n    \n    @discardableResult\n    func banPersonFromInstance(\n        personId: Int,\n        ban: Bool,\n        removeContent: Bool,\n        reason: String?,\n        expires: Date? = nil\n    ) async throws -> Person {\n        let snapshot = try await repository.banPersonFromInstance(\n            personId: personId,\n            ban: ban,\n            removeContent: removeContent,\n            reason: reason,\n            expires: expires\n        )\n        return await caches.person.getModel(\n            api: self,\n            from: .person2(snapshot)\n        )\n    }\n    \n    func purgePerson(id: Int, reason: String?) async throws {\n        try await repository.purgePerson(id: id, reason: reason)\n        caches.person.retrieveModel(cacheId: id)?.purged = true\n    }\n    \n    func getContent(\n        authorId id: Int,\n        sort: PostSortType,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool? = nil,\n        communityId: Int? = nil\n    ) async throws -> (person: Person, posts: [Post], comments: [Comment]) {\n        let snapshots = try await repository.getContent(\n            authorId: id,\n            sort: sort,\n            page: page,\n            limit: limit,\n            savedOnly: savedOnly,\n            communityId: communityId\n        )\n        return await (\n            person: caches.person.getModel(api: self, from: .person3(snapshots.person)),\n            posts: caches.post.getModels(api: self, from: snapshots.posts.map { .post2($0) }),\n            comments: caches.comment.getModels(api: self, from: snapshots.comments.map { .comment2($0) })\n        )\n    }\n    \n    func getMyPerson() async throws -> (person: Person?, instance: Instance, blocks: BlockList?) {\n        let snapshot = try await repository.getMyPerson()\n        let snapshotPersonName = snapshot.person?.person.person.person.name\n        guard snapshotPersonName == username else {\n            assertionFailure(\n                \"Returned account name \\(String(describing: snapshotPersonName)) does not match logged in username \\(String(describing: username))\"\n            )\n            throw ApiClientError.mismatchingToken\n        }\n        \n        let instance = await caches.instance.getModel(api: self, from: .instance3(snapshot.instance))\n        let person = await caches.person.getOptionalModel(api: self, from: .person4(snapshot.person))\n        var blocks: BlockList? = blocks\n        \n        if person != nil, let newBlocks = snapshot.blocks {\n            if let blocks {\n                blocks.update(blocks: newBlocks)\n            } else {\n                blocks = .init(api: self, blocks: newBlocks)\n            }\n        }\n        _ = await Task { @MainActor in\n            self.blocks = blocks\n            myPerson = person\n            myInstance = instance\n        }.result\n        return (person: person, instance: instance, blocks: blocks)\n    }\n    \n    func deleteAccount(password: String, deleteContent: Bool) async throws {\n        try await repository.deleteAccount(password: password, deleteContent: deleteContent)\n    }\n\n    func editProfile(_ details: ProfileDetails) async throws {\n        try await repository.editProfile(details)\n    }\n    \n    func editAccountSettings(\n        showNsfw: Bool?,\n        showScores: Bool?,\n        theme: String?,\n        defaultListingType: ListingType?,\n        interfaceLanguage: String?,\n        avatar: String?,\n        banner: String?,\n        displayName: String?,\n        email: String?,\n        bio: String?,\n        matrixUserId: String?,\n        showAvatars: Bool?,\n        sendNotificationsToEmail: Bool?,\n        botAccount: Bool?,\n        showBotAccounts: Bool?,\n        showReadPosts: Bool?,\n        discussionLanguages: [Int]?,\n        openLinksInNewTab: Bool?,\n        blurNsfw: Bool?,\n        autoExpand: Bool?,\n        infiniteScrollEnabled: Bool?,\n        postListingMode: PostFeedViewMode?,\n        enableKeyboardNavigation: Bool?,\n        enableAnimatedImages: Bool?,\n        collapseBotComments: Bool?,\n        showUpvotes: Bool?,\n        showDownvotes: Bool?,\n        showUpvotePercentage: Bool?\n    ) async throws {\n        try await repository.editAccountSettings(\n            showNsfw: showNsfw,\n            showScores: showScores,\n            theme: theme,\n            defaultListingType: defaultListingType,\n            interfaceLanguage: interfaceLanguage,\n            avatar: avatar,\n            banner: banner,\n            displayName: displayName,\n            email: email,\n            bio: bio,\n            matrixUserId: matrixUserId,\n            showAvatars: showAvatars,\n            sendNotificationsToEmail: sendNotificationsToEmail,\n            botAccount: botAccount,\n            showBotAccounts: showBotAccounts,\n            showReadPosts: showReadPosts,\n            discussionLanguages: discussionLanguages,\n            openLinksInNewTab: openLinksInNewTab,\n            blurNsfw: blurNsfw,\n            autoExpand: autoExpand,\n            infiniteScrollEnabled: infiniteScrollEnabled,\n            postListingMode: postListingMode,\n            enableKeyboardNavigation: enableKeyboardNavigation,\n            enableAnimatedImages: enableAnimatedImages,\n            collapseBotComments: collapseBotComments,\n            showUpvotes: showUpvotes,\n            showDownvotes: showDownvotes,\n            showUpvotePercentage: showUpvotePercentage\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+Post.swift",
    "content": "//\n//  NewApiClient+Post.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/02/2024.\n//\n\nimport Foundation\n\npublic extension ApiClient {\n    // swiftlint:disable:next function_parameter_count\n    func getPosts(\n        communityId: Int,\n        sort: PostSortType,\n        page: Int,\n        cursor: String?,\n        limit: Int,\n        filter: GetContentFilter? = nil,\n        showHidden: Bool = false\n    ) async throws -> (posts: [Post], cursor: String?) {\n        let snapshots = try await repository.getPosts(\n            communityId: communityId,\n            sort: sort,\n            page: page,\n            cursor: cursor,\n            limit: limit,\n            filter: filter,\n            showHidden: showHidden\n        )\n        let posts = await caches.post.getModels(\n            api: self,\n            from: snapshots.posts.map { .post2($0) }\n        )\n        return (posts: posts, cursor: snapshots.cursor)\n    }\n    \n    // swiftlint:disable:next function_parameter_count\n    func getPosts(\n        feed: ListingType,\n        sort: PostSortType,\n        page: Int,\n        cursor: String?,\n        limit: Int,\n        filter: GetContentFilter? = nil,\n        showHidden: Bool = false\n    ) async throws -> (posts: [Post], cursor: String?) {\n        let snapshots = try await repository.getPosts(\n            feed: feed,\n            sort: sort,\n            page: page,\n            cursor: cursor,\n            limit: limit,\n            filter: filter,\n            showHidden: showHidden\n        )\n        let posts = await caches.post.getModels(\n            api: self,\n            from: snapshots.posts.map { .post2($0) }\n        )\n        return (posts: posts, cursor: snapshots.cursor)\n    }\n    \n    func getPosts(\n        personId: Int,\n        communityId: Int? = nil,\n        sort: PostSortType = .new,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool = false\n    ) async throws -> (person: Person, posts: [Post]) {\n        let snapshots = try await repository.getPosts(\n            personId: personId,\n            communityId: communityId,\n            sort: sort,\n            page: page,\n            limit: limit,\n            savedOnly: savedOnly\n        )\n        return await (\n            person: caches.person.getModel(api: self, from: .person3(snapshots.person)),\n            posts: caches.post.getModels(api: self, from: snapshots.posts.map { .post2($0) })\n        )\n    }\n    \n    func getPostHistory(\n        type: GetContentFilter,\n        page: Int?,\n        cursor: String?,\n        limit: Int\n    ) async throws -> (posts: [Post], cursor: String?) {\n        let snapshots = try await repository.getPostHistory(\n            type: type,\n            page: page,\n            cursor: cursor,\n            limit: limit\n        )\n        let posts = await caches.post.getModels(\n            api: self,\n            from: snapshots.posts.map { .post2($0) }\n        )\n        return (posts: posts, cursor: snapshots.cursor)\n    }\n    \n    func getPost(id: Int) async throws -> Post {\n        let snapshot = try await repository.getPost(id: id)\n        return await caches.post.getModel(api: self, from: .post3(snapshot))\n    }\n    \n    func getPost(url: URL) async throws -> Post {\n        let snapshot = try await repository.getPost(url: url)\n        return await caches.post.getModel(api: self, from: .post2(snapshot))\n    }\n    \n    // This method should be removed in favor of the below method once we drop support for versions before Lemmy 1.0\n    func searchPosts(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: PostSortType\n    ) async throws -> [Post] {\n        let snapshots = try await repository.searchPosts(\n            query: query,\n            page: page,\n            limit: limit,\n            communityId: communityId,\n            creatorId: creatorId,\n            filter: filter,\n            sort: sort\n        )\n        return await caches.post.getModels(api: self, from: snapshots.map { .post2($0) })\n    }\n    \n    func searchPosts(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: SearchSortType\n    ) async throws -> [Post] {\n        let snapshots = try await repository.searchPosts(\n            query: query,\n            page: page,\n            limit: limit,\n            communityId: communityId,\n            creatorId: creatorId,\n            filter: filter,\n            sort: sort\n        )\n        return await caches.post.getModels(api: self, from: snapshots.map { .post2($0) })\n    }\n    \n    /// Mark the given posts as read.\n    /// Calling this will also mark any queued posts as read unless `includeQueuedPosts` is set to `false`.\n    func markPostsAsRead(\n        ids: Set<Int>,\n        includeQueuedPosts: Bool = true\n    ) async throws {\n        let idsToSend: Set<Int>\n        let markReadQueueCopy: Set<Int>\n        if includeQueuedPosts {\n            markReadQueueCopy = await markReadQueue.popAll()\n            idsToSend = ids.union(markReadQueueCopy)\n        } else {\n            markReadQueueCopy = []\n            idsToSend = ids\n        }\n        \n        guard !idsToSend.isEmpty else { return }\n        \n        do {\n            try await repository.markPostsAsRead(ids: idsToSend)\n            await markReadQueue.subtract(ids)\n        } catch {\n            await markReadQueue.union(markReadQueueCopy)\n            throw error\n        }\n        Task { @MainActor in\n            for post in idsToSend.compactMap({ caches.post.retrieveModel(cacheId: $0) }) {\n                post.queuedMarkReadCompleted()\n            }\n        }\n    }\n    \n    func flushPostReadQueue() async throws {\n        if await !markReadQueue.ids.isEmpty {\n            try await markPostsAsRead(ids: [])\n        }\n    }\n    \n    func createPost(\n        communityId: Int,\n        title: String,\n        content: String? = nil,\n        linkUrl: URL? = nil,\n        altText: String? = nil,\n        thumbnail: URL? = nil,\n        nsfw: Bool,\n        languageId: Int? = nil\n    ) async throws -> Post {\n        let snapshot = try await repository.createPost(\n            communityId: communityId,\n            title: title,\n            content: content,\n            linkUrl: linkUrl,\n            altText: altText,\n            thumbnail: thumbnail,\n            nsfw: nsfw,\n            languageId: languageId\n        )\n        return await caches.post.getModel(api: self, from: .post2(snapshot))\n    }\n    \n    func replyToPost(id: Int, content: String, languageId: Int? = nil) async throws -> Comment {\n        let snapshot = try await repository.replyToPost(id: id, content: content, languageId: languageId)\n        return await caches.comment.getModel(api: self, from: .comment2(snapshot))\n    }\n    \n    @discardableResult\n    func reportPost(id: Int, reason: String) async throws -> Report {\n        let snapshot = try await repository.reportPost(id: id, reason: reason)\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.report.getModel(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId\n        )\n    }\n    \n    func purgePost(id: Int, reason: String?) async throws {\n        try await repository.purgePost(id: id, reason: reason)\n    }\n    \n    @discardableResult\n    func getPostVotes(\n        id: Int,\n        communityId: Int,\n        page: Int = 1,\n        limit: Int = 20\n    ) async throws -> [PersonVote] {\n        let snapshot = try await repository.getPostVotes(\n            id: id,\n            page: page,\n            limit: limit\n        )\n        return await caches.personVote.getModels(\n            api: self,\n            from: snapshot,\n            target: .post(id: id),\n            communityId: communityId\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+RegistrationApplication.swift",
    "content": "//\n//  ApiClient+RegistrationApplication.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-01-12.\n//\n\nimport Foundation\n\npublic extension ApiClient {\n    func getRegistrationApplicationCount() async throws -> Int {\n        try await repository.getRegistrationApplicationCount()\n    }\n    \n    func getRegistrationApplications(\n        page: Int = 1,\n        limit: Int = 20,\n        unreadOnly: Bool = false\n    ) async throws -> [RegistrationApplication] {\n        let snapshot = try await repository.getRegistrationApplications(\n            page: page,\n            limit: limit,\n            unreadOnly: unreadOnly\n        )\n        return await caches.registrationApplication.getModels(api: self, from: snapshot)\n    }\n    \n    @discardableResult\n    func approveRegistrationApplication(\n        id: Int,\n        semaphore: UInt? = nil\n    ) async throws -> RegistrationApplication {\n        let snapshot = try await repository.approveRegistrationApplication(id: id)\n        return await caches.registrationApplication.getModel(\n            api: self,\n            from: snapshot,\n            semaphore: semaphore\n        )\n    }\n    \n    @discardableResult\n    func denyRegistrationApplication(\n        id: Int,\n        reason: String?,\n        semaphore: UInt? = nil\n    ) async throws -> RegistrationApplication {\n        let snapshot = try await repository.denyRegistrationApplication(id: id, reason: reason)\n        return await caches.registrationApplication.getModel(\n            api: self,\n            from: snapshot,\n            semaphore: semaphore\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient+Report.swift",
    "content": "//\n//  ApiClient+Report.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-16.\n//\n\nimport Foundation\n\npublic extension ApiClient {\n    func getPostReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false,\n        communityId: Int? = nil,\n        postId: Int? = nil\n    ) async throws -> [Report] {\n        let snapshot = try await repository.getPostReports(\n            page: page,\n            limit: limit,\n            unresolvedOnly: unresolvedOnly,\n            communityId: communityId,\n            postId: postId\n        )\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.report.getModels(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId\n        )\n    }\n    \n    func getCommentReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false,\n        communityId: Int? = nil,\n        commentId: Int? = nil\n    ) async throws -> [Report] {\n        let snapshot = try await repository.getCommentReports(\n            page: page,\n            limit: limit,\n            unresolvedOnly: unresolvedOnly,\n            communityId: communityId,\n            commentId: commentId\n        )\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.report.getModels(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId\n        )\n    }\n    \n    func getMessageReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false\n    ) async throws -> [Report] {\n        let snapshot = try await repository.getMessageReports(\n            page: page,\n            limit: limit,\n            unresolvedOnly: unresolvedOnly\n        )\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.report.getModels(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId\n        )\n    }\n    \n    @discardableResult\n    func resolvePostReport(\n        id: Int,\n        resolved: Bool,\n        semaphore: UInt? = nil\n    ) async throws -> Report {\n        let snapshot = try await repository.resolvePostReport(id: id, resolved: resolved)\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.report.getModel(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId,\n            semaphore: semaphore\n        )\n    }\n    \n    @discardableResult\n    func resolveCommentReport(\n        id: Int,\n        resolved: Bool,\n        semaphore: UInt? = nil\n    ) async throws -> Report {\n        let snapshot = try await repository.resolveCommentReport(id: id, resolved: resolved)\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.report.getModel(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId,\n            semaphore: semaphore\n        )\n    }\n    \n    @discardableResult\n    func resolveMessageReport(\n        id: Int,\n        resolved: Bool,\n        semaphore: UInt? = nil\n    ) async throws -> Report {\n        let snapshot = try await repository.resolveMessageReport(id: id, resolved: resolved)\n        guard let myPersonId = try await myPersonId else { throw ApiClientError.notLoggedIn }\n        return await caches.report.getModel(\n            api: self,\n            from: snapshot,\n            myPersonId: myPersonId,\n            semaphore: semaphore\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClient.swift",
    "content": "//\n//  ApiClient.swift\n//  Mlem\n//\n//  Created by Sjmarf on 10/02/2024.\n//\n\nimport Combine\nimport Foundation\nimport os\nimport Rest\n\n@Observable\npublic class ApiClient {\n    let log: Logger = .mlemLogger()\n    \n    var repository: ApiRepository\n    \n    public var willSendToken: Bool { repository.token != nil }\n    \n    public internal(set) weak var myInstance: Instance?\n    public internal(set) weak var myPerson: Person?\n    public internal(set) weak var subscriptions: SubscriptionList?\n    public internal(set) weak var blocks: BlockList?\n    public internal(set) weak var unreadCount: UnreadCount?\n    \n    /// Stores the IDs of posts that are queued to be marked read.\n    var markReadQueue: MarkReadQueue = .init()\n    \n    public func ensureContextPresence() async throws {\n        try await repository.getConnection().ensureContextPresence()\n    }\n    \n    public func supports(_ feature: Feature) async throws -> Bool {\n        try await repository.getConnection().supports(feature)\n    }\n    \n    /// Returns whether this `ApiClient` supports the given feature. If this information cannot be resolved, returns the provided `defaultValue`\n    public func supports(_ feature: Feature, defaultValue: Bool) -> Bool {\n        repository.connection?.supports(feature, defaultValue: defaultValue) ?? defaultValue\n    }\n    \n    public var contextIsFetched: Bool {\n        repository.connection?.contextIsFetched ?? false\n    }\n\n    public var username: String? { repository.username }\n    \n    public var baseUrl: URL { repository.baseUrl }\n    \n    public var token: String? { repository.token }\n    \n    public var myPersonId: Int? {\n        get async throws {\n            try await repository.getConnection().myPersonId\n        }\n    }\n    \n    public var software: SiteSoftware {\n        get async throws {\n            let connection = try await repository.getConnection()\n            return try await .init(type: type(of: connection).softwareType, version: connection.version)\n        }\n    }\n    \n    public var voteFederationMode: VoteFederationMode {\n        myInstance?.voteFederationMode.value ?? .all\n    }\n    \n    // MARK: caching\n    \n    /// Caches of objects stored per ApiClient instance\n    /// - Warning: DO NOT access this outside of ApiClient!\n    var caches: BaseCacheGroup = .init()\n    \n    /// Caches of Instance objects, shared across all ApiClient instances\n    /// - Warning: DO NOT access this outside of ApiClient!\n    static var apiClientCache: ApiClientCache = .init()\n    \n    /// Creates or retrieves an API client for the given connection parameters\n    public static func getApiClient(url: URL, username: String?) -> ApiClient {\n        apiClientCache.createOrRetrieveApiClient(url: url, username: username)\n    }\n    \n    /// This should never be used outside of ApiClientCache (and MockApiClient), as the caching system depends on one ApiClient existing for any given session.\n    init(\n        url: URL,\n        username: String? = nil\n    ) {\n        self.repository = .init(baseUrl: url, username: username)\n    }\n    \n    public func cleanCaches() {\n        caches.clean()\n        ApiClient.apiClientCache.clean()\n    }\n    \n    /// Return a new guest `ApiClient`.\n    public func asGuest() -> ApiClient {\n        .getApiClient(url: baseUrl, username: nil)\n    }\n    \n    /// Return a new `ApiClient` targeting the given user.\n    public func asUser(name: String) -> ApiClient {\n        .getApiClient(url: baseUrl, username: name)\n    }\n    \n    /// This should **only** be used when we get a new token for **the same** account!\n    public func updateToken(_ newToken: String) {\n        repository.updateToken(newToken)\n    }\n}\n\nextension ApiClient: CacheIdentifiable {\n    public var cacheId: Int {\n        ApiClient.apiClientCache.getCacheId(url: baseUrl, username: username)\n    }\n}\n\nextension ApiClient: ActorIdentifiable {\n    public var actorId: ActorIdentifier { .instance(host: baseUrl.host()!) }\n}\n\nextension ApiClient: Hashable {\n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(baseUrl)\n        hasher.combine(username)\n    }\n    \n    public static func == (lhs: ApiClient, rhs: ApiClient) -> Bool {\n        lhs === rhs\n    }\n}\n\nextension ApiClient: CustomDebugStringConvertible {\n    public var debugDescription: String {\n        \"ApiClient(\\(host), authenticated: \\(repository.token != nil))\"\n    }\n}\n\n// MARK: ApiClientCache\n\n// This needs to be declared in this file to have access to the private initializer\n\nextension ApiClient {\n    /// Cache for ApiClient--exception case because there's no ApiType and it may need to perform ApiClient bootstrapping\n    class ApiClientCache: CoreCache<ApiClient> {\n        func getCacheId(url: URL, username: String?) -> Int {\n            var hasher: Hasher = .init()\n            hasher.combine(url.removingPathComponents().appendingPathComponent(\"/\"))\n            hasher.combine(username)\n            return hasher.finalize()\n        }\n\n        func createOrRetrieveApiClient(url: URL, username: String?) -> ApiClient {\n            let url = url.removingPathComponents().appendingPathComponent(\"/\")\n            if let client = retrieveModel(cacheId: getCacheId(url: url, username: username)) {\n                return client\n            }\n            \n            let ret: ApiClient = .init(url: url, username: username)\n            itemCache.put(ret)\n            return ret\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiClientError.swift",
    "content": "//\n//  ApiClientError.swift\n//  Mlem\n//\n//  Created by Sjmarf on 17/02/2024.\n//\n\nimport Foundation\nimport Rest\n\nenum HTTPMethod {\n    case get\n    case post(Data)\n}\n\npublic enum ApiClientError: Error {\n    case encoding(Error)\n    case networking(Error)\n    case serverError(statusCode: Int)\n    case response(ApiErrorResponse, Int)\n    case cancelled\n    case notLoggedIn\n    case invalidSession\n    case decoding(Data, Error?)\n    case insufficientPermissions\n    /// Thrown when a `false` value of `SuccessResponse` is returned\n    case unsuccessful\n    case featureUnsupported\n    case noEntityFound\n    case invalidInput\n    case imageTooLarge\n    case mismatchingUrl\n    case mismatchingPersonId\n    case mismatchingToken\n    case noToken\n    case responseMissingRequiredData(_ message: String)\n    case unableToDetermineSoftware\n    \n    init(from error: RestError) {\n        self = switch error {\n        case let .serverError(statusCode):\n            .serverError(statusCode: statusCode)\n        case let .response(string, statusCode):\n            .response(.init(error: string), statusCode)\n        case let .encoding(error):\n            .encoding(error)\n        case let .parameterEncoding(error):\n            .encoding(error)\n        case let .decoding(data, error):\n            .decoding(data, error)\n        case let .networking(error):\n            .networking(error)\n        case .cancelled:\n            .cancelled\n        }\n    }\n}\n\nextension ApiClientError: CustomStringConvertible {\n    public var description: String {\n        switch self {\n        case .insufficientPermissions:\n            return \"Insufficient permissions. Check `ApiClient.permissions`\"\n        case let .encoding(error):\n            return \"Unable to encode: \\(String(describing: error))\"\n        case let .networking(error):\n            return \"Networking error: \\(String(describing: error))\"\n        case let .response(errorResponse, status):\n            return \"Response error: \\(errorResponse) with status \\(status)\"\n        case let .serverError(status):\n            return \"Server error: \\(status)\"\n        case .cancelled:\n            return \"Cancelled\"\n        case .invalidSession:\n            return \"Invalid session. There is a token applied to the ApiClient, but it has expired.\"\n        case .notLoggedIn:\n            return \"Tried to perform an action that requires authentication on a guest ApiClient.\"\n        case .imageTooLarge:\n            return \"Image too large\"\n        case let .decoding(data, error):\n            guard let string = String(data: data, encoding: .utf8) else {\n                return localizedDescription\n            }\n            \n            if let error {\n                return \"Unable to decode: \\(string)\\nError: \\(String(describing: error))\"\n            }\n            \n            return \"Unable to decode: \\(string)\"\n        case .unsuccessful:\n            return \"Operation was unsuccessful.\"\n        case .featureUnsupported:\n            return \"This instance doesn't support that operation.\"\n        case .noEntityFound:\n            return \"No entity returned in response.\"\n        case .invalidInput:\n            return \"Invalid input\"\n        case .mismatchingUrl:\n            return \"URL of the decoding ApiClient doesn't match the URL of the ApiClient that encoded the data\"\n        case .mismatchingPersonId:\n            return \"Person ID of the decoding ApiClient doesn't match the Person ID of the ApiClient that encoded the data\"\n        case .mismatchingToken:\n            return \"A valid token was assigned to an ApiClient for the wrong account.\"\n        case .noToken:\n            return \"A call was made to an ApiClient that doesn't have a token yet.\"\n        case let .responseMissingRequiredData(message):\n            return \"An API response was missing required data: \\(message)\"\n        case .unableToDetermineSoftware:\n            return \"Unable to determine software\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiErrorResponse.swift",
    "content": "//\n//  ApiErrorResponse.swift\n//  Mlem\n//\n//  Created by Nicholas Lawson on 06/06/2023.\n//\n\nimport Foundation\n\n// TODO: 0.19 support add all the error types (https://github.com/LemmyNet/lemmy-js-client/blob/b2edfeeaffd189a51150362cc8ead03c65ee2652/src/types/LemmyErrorType.ts)\n\npublic struct ApiErrorResponse: Decodable, CustomStringConvertible {\n    public let error: String\n    \n    public var description: String { error }\n}\n\nprivate let possibleCredentialErrors: Set<String> = [\n    \"incorrect_password\",\n    \"password_incorrect\",\n    \"incorrect_login\",\n    \"couldnt_find_that_username_or_email\"\n]\n\nprivate let possibleAuthenticationErrors: Set<String> = [\n    \"incorrect_password\",\n    \"password_incorrect\",\n    \"incorrect_login\",\n    \"not_logged_in\"\n]\n\nprivate let possible2FAErrors: Set<String> = [\n    \"missing_totp_token\",\n    \"incorrect_totp_token\"\n]\n\nprivate let couldntFindObjectErrors: Set<String> = [\n    \"couldnt_find_person\",\n    \"couldnt_find_object\",\n    \"No object found.\"\n]\n\npublic extension ApiErrorResponse {\n    var requires2FA: Bool { possible2FAErrors.contains(error) }\n    var isNotLoggedIn: Bool { possibleAuthenticationErrors.contains(error) }\n    var instanceIsPrivate: Bool { error == \"instance_is_private\" }\n    var registrationApplicationIsPending: Bool { error == \"registration_application_is_pending\" }\n    var emailNotVerified: Bool { error == \"email_not_verified\" }\n    var couldntFindObject: Bool { couldntFindObjectErrors.contains(error) }\n    var notModOrAdmin: Bool { error == \"not_a_mod_or_admin\" }\n    var notAdmin: Bool { error == \"not_an_admin\" }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/ApiSession.swift",
    "content": "//\n//  ApiSession.swift\n//  Mlem\n//\n//  Created by mormaer on 02/09/2023.\n//\n//\n\nimport Foundation\n\nenum ApiSessionError: Error {\n    case authenticationNotPresent\n    case undefined\n}\n\n/// An enumeration representing possible session states\nenum ApiSession {\n    case authenticated(URL, String)\n    case unauthenticated(URL)\n    case undefined\n    \n    var token: String {\n        get throws {\n            guard case let .authenticated(_, token) = self else {\n                throw ApiSessionError.authenticationNotPresent\n            }\n            \n            return token\n        }\n    }\n    \n    var instanceUrl: URL {\n        get throws {\n            switch self {\n            case let .authenticated(url, _):\n                return url\n            case let .unauthenticated(url):\n                return url\n            case .undefined:\n                throw ApiSessionError.undefined\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/ApiTypeBackedCache.swift",
    "content": "//\n//  ContentCache.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-01.\n//\n\nimport Foundation\n\n/// Class providing caching behavior for models associated with API types\nclass ApiTypeBackedCache<Content: CacheIdentifiable & AnyObject & ContentModel, ApiType: CacheIdentifiable>: CoreCache<Content> {\n    // TODO: Unified RegistrationApplication remove semaphore\n    @MainActor func getModel(\n        api: ApiClient,\n        from apiType: ApiType,\n        // If `true`, the model will not be updated with the incoming data if the model already exists.\n        isStale: Bool = false,\n        semaphore: UInt? = nil\n    ) -> Content {\n        if let item = retrieveModel(cacheId: apiType.cacheId) {\n            if !isStale {\n                updateModel(item, with: apiType, semaphore: semaphore)\n            }\n            return item\n        }\n        \n        let newItem: Content = performModelTranslation(api: api, from: apiType)\n        itemCache.put(newItem)\n        return newItem\n    }\n    \n    @MainActor\n    func getModels(\n        api: ApiClient,\n        from apiTypes: any Sequence<ApiType>,\n        isStale: Bool = false,\n        semaphore: UInt? = nil\n    ) -> [Content] {\n        apiTypes.map { getModel(api: api, from: $0, isStale: isStale, semaphore: semaphore) }\n    }\n    \n    @MainActor\n    func getOptionalModel(\n        api: ApiClient,\n        from apiType: ApiType?,\n        isStale: Bool = false,\n        semaphore: UInt? = nil\n    ) -> Content? {\n        if let apiType {\n            return getModel(api: api, from: apiType, isStale: isStale, semaphore: semaphore)\n        }\n        return nil\n    }\n    \n    /// Initializes a new middleware model from the associated API type\n    /// - Warning: This method DOES NOT CACHE! You almost certainly want to be using `getModel` instead.\n    @MainActor\n    func performModelTranslation(api: ApiClient, from apiType: ApiType) -> Content {\n        // the name of this method is intentionally unwieldy to further discourage accidental use\n        preconditionFailure(\"This method must be overridden by the instantiating class: \\(self)\")\n    }\n    \n    @MainActor\n    func updateModel(_ item: Content, with apiType: ApiType, semaphore: UInt? = nil) {\n        preconditionFailure(\"This method must be overridden by the instantiating class: \\(self)\")\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Atomic.swift",
    "content": "//\n//  Atomic.swift\n//  ReactiveSwift\n//\n//  Created by Justin Spahr-Summers on 2014-06-10.\n//  Copyright (c) 2014 GitHub. All rights reserved.\n//\n// See NOTICE.md for license\n\nimport Foundation\n#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)\n    import MachO\n#endif\n\n/// A simple, generic lock-free finite state machine.\n///\n/// - warning: `deinitialize` must be called to dispose of the consumed memory.\nprivate struct UnsafeAtomicState<State: RawRepresentable> where State.RawValue == Int32 {\n    typealias Transition = (expected: State, next: State)\n    #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)\n        private let value: UnsafeMutablePointer<Int32>\n\n        /// Create a finite state machine with the specified initial state.\n        ///\n        /// - parameters:\n        ///   - initial: The desired initial state.\n        init(_ initial: State) {\n            self.value = UnsafeMutablePointer<Int32>.allocate(capacity: 1)\n            value.initialize(to: initial.rawValue)\n        }\n\n        /// Deinitialize the finite state machine.\n        func deinitialize() {\n            value.deinitialize(count: 1)\n            value.deallocate()\n        }\n\n        /// Compare the current state with the specified state.\n        ///\n        /// - parameters:\n        ///   - expected: The expected state.\n        ///\n        /// - returns: `true` if the current state matches the expected state.\n        ///            `false` otherwise.\n        func `is`(_ expected: State) -> Bool {\n            expected.rawValue == value.pointee\n        }\n\n        /// Try to transition from the expected current state to the specified next\n        /// state.\n        ///\n        /// - parameters:\n        ///   - expected: The expected state.\n        ///   - next: The state to transition to.\n        ///\n        /// - returns: `true` if the transition succeeds. `false` otherwise.\n        func tryTransition(from expected: State, to next: State) -> Bool {\n            OSAtomicCompareAndSwap32Barrier(\n                expected.rawValue,\n                next.rawValue,\n                value\n            )\n        }\n    #else\n        private let value: Atomic<Int32>\n\n        /// Create a finite state machine with the specified initial state.\n        ///\n        /// - parameters:\n        ///   - initial: The desired initial state.\n        init(_ initial: State) {\n            self.value = Atomic(initial.rawValue)\n        }\n\n        /// Deinitialize the finite state machine.\n        func deinitialize() {}\n\n        /// Compare the current state with the specified state.\n        ///\n        /// - parameters:\n        ///   - expected: The expected state.\n        ///\n        /// - returns: `true` if the current state matches the expected state.\n        ///            `false` otherwise.\n        func `is`(_ expected: State) -> Bool {\n            value.value == expected.rawValue\n        }\n\n        /// Try to transition from the expected current state to the specified next\n        /// state.\n        ///\n        /// - parameters:\n        ///   - expected: The expected state.\n        ///\n        /// - returns: `true` if the transition succeeds. `false` otherwise.\n        func tryTransition(from expected: State, to next: State) -> Bool {\n            value.modify { value in\n                if value == expected.rawValue {\n                    value = next.rawValue\n                    return true\n                }\n                return false\n            }\n        }\n    #endif\n}\n\n/// `Lock` exposes `os_unfair_lock` on supported platforms, with pthread mutex as the\n/// fallback.\nprivate class Lock {\n    #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)\n        @available(iOS 10.0, *)\n        @available(macOS 10.12, *)\n        @available(tvOS 10.0, *)\n        @available(watchOS 3.0, *)\n        final class UnfairLock: Lock {\n            private let _lock: os_unfair_lock_t\n\n            override init() {\n                self._lock = .allocate(capacity: 1)\n                _lock.initialize(to: os_unfair_lock())\n                super.init()\n            }\n\n            override func lock() {\n                os_unfair_lock_lock(_lock)\n            }\n\n            override func unlock() {\n                os_unfair_lock_unlock(_lock)\n            }\n\n            override func `try`() -> Bool {\n                os_unfair_lock_trylock(_lock)\n            }\n\n            deinit {\n                _lock.deinitialize(count: 1)\n                _lock.deallocate()\n            }\n        }\n    #endif\n\n    final class PthreadLock: Lock {\n        private let _lock: UnsafeMutablePointer<pthread_mutex_t>\n\n        init(recursive: Bool = false) {\n            self._lock = .allocate(capacity: 1)\n            _lock.initialize(to: pthread_mutex_t())\n\n            let attr = UnsafeMutablePointer<pthread_mutexattr_t>.allocate(capacity: 1)\n            attr.initialize(to: pthread_mutexattr_t())\n            pthread_mutexattr_init(attr)\n\n            defer {\n                pthread_mutexattr_destroy(attr)\n                attr.deinitialize(count: 1)\n                attr.deallocate()\n            }\n\n            pthread_mutexattr_settype(attr, Int32(recursive ? PTHREAD_MUTEX_RECURSIVE : PTHREAD_MUTEX_ERRORCHECK))\n\n            let status = pthread_mutex_init(_lock, attr)\n            assert(status == 0, \"Unexpected pthread mutex error code: \\(status)\")\n\n            super.init()\n        }\n\n        override func lock() {\n            let status = pthread_mutex_lock(_lock)\n            assert(status == 0, \"Unexpected pthread mutex error code: \\(status)\")\n        }\n\n        override func unlock() {\n            let status = pthread_mutex_unlock(_lock)\n            assert(status == 0, \"Unexpected pthread mutex error code: \\(status)\")\n        }\n\n        override func `try`() -> Bool {\n            let status = pthread_mutex_trylock(_lock)\n            switch status {\n            case 0:\n                return true\n            case EBUSY, EAGAIN:\n                return false\n            default:\n                assertionFailure(\"Unexpected pthread mutex error code: \\(status)\")\n                return false\n            }\n        }\n\n        deinit {\n            let status = pthread_mutex_destroy(_lock)\n            assert(status == 0, \"Unexpected pthread mutex error code: \\(status)\")\n\n            _lock.deinitialize(count: 1)\n            _lock.deallocate()\n        }\n    }\n\n    static func make() -> Lock {\n        #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)\n            if #available(*, iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0) {\n                return UnfairLock()\n            }\n        #endif\n\n        return PthreadLock()\n    }\n\n    private init() {}\n\n    func lock() { fatalError() }\n    func unlock() { fatalError() }\n    func `try`() -> Bool { fatalError() }\n}\n\n/// An atomic variable.\npublic final class Atomic<Value> {\n    private let lock: Lock\n    private var _value: Value\n\n    /// Atomically get or set the value of the variable.\n    public var value: Value {\n        get {\n            withValue { $0 }\n        }\n\n        set(newValue) {\n            swap(newValue)\n        }\n    }\n\n    /// Initialize the variable with the given initial value.\n    ///\n    /// - parameters:\n    ///   - value: Initial value for `self`.\n    public init(_ value: Value) {\n        self._value = value\n        self.lock = Lock.make()\n    }\n\n    /// Atomically modifies the variable.\n    ///\n    /// - parameters:\n    ///   - action: A closure that takes the current value.\n    ///\n    /// - returns: The result of the action.\n    @discardableResult\n    public func modify<Result>(_ action: (inout Value) throws -> Result) rethrows -> Result {\n        lock.lock()\n        defer { lock.unlock() }\n\n        return try action(&_value)\n    }\n\n    /// Atomically perform an arbitrary action using the current value of the\n    /// variable.\n    ///\n    /// - parameters:\n    ///   - action: A closure that takes the current value.\n    ///\n    /// - returns: The result of the action.\n    @discardableResult\n    public func withValue<Result>(_ action: (Value) throws -> Result) rethrows -> Result {\n        lock.lock()\n        defer { lock.unlock() }\n\n        return try action(_value)\n    }\n\n    /// Atomically replace the contents of the variable.\n    ///\n    /// - parameters:\n    ///   - newValue: A new value for the variable.\n    ///\n    /// - returns: The old value.\n    @discardableResult\n    public func swap(_ newValue: Value) -> Value {\n        modify { (value: inout Value) in\n            let oldValue = value\n            value = newValue\n            return oldValue\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/CommentCaches.swift",
    "content": "//\n//  CommentCache.swift\n//\n//\n//  Created by Sjmarf on 24/06/2024.\n//\n\nimport Foundation\n\npublic enum AnyCommentSnapshot: CacheIdentifiable {\n    case comment1(Comment1Snapshot)\n    case comment2(Comment2Snapshot)\n    \n    public var cacheId: Int {\n        switch self {\n        case let .comment1(snapshot): snapshot.cacheId\n        case let .comment2(snapshot): snapshot.cacheId\n        }\n    }\n}\n\nclass CommentCache: ApiTypeBackedCache<Comment, AnyCommentSnapshot> {\n    override func performModelTranslation(api: ApiClient, from apiType: AnyCommentSnapshot) -> Comment {\n        return .init(api: api, properties: .init(api: api, snapshot: apiType))\n    }\n    \n    override func updateModel(_ item: Comment, with apiType: AnyCommentSnapshot, semaphore: UInt? = nil) {\n        // attempt a direct update through the queue to avoid overwriting more recent data, and also\n        // synchronously perform softUpdate to ensure high-tier data is available where expected\n        let properties: CommentProperties = .init(api: item.api, snapshot: apiType)\n        Task {\n            await item.updateQueue.attemptDirectUpdate(with: properties)\n        }\n        item.softUpdate(with: properties)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/CommunityCache.swift",
    "content": "//\n//  CommunityCache.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-01.\n//\n\nimport Foundation\n\npublic enum AnyCommunitySnapshot: CacheIdentifiable {\n    case community1(Community1Snapshot)\n    case community2(Community2Snapshot)\n    case community3(Community3Snapshot)\n    \n    public var cacheId: Int {\n        switch self {\n        case let .community1(snapshot): snapshot.cacheId\n        case let .community2(snapshot): snapshot.cacheId\n        case let .community3(snapshot): snapshot.cacheId\n        }\n    }\n}\n\nclass CommunityCache: ApiTypeBackedCache<Community, AnyCommunitySnapshot> {\n    override func performModelTranslation(api: ApiClient, from apiType: AnyCommunitySnapshot) -> Community {\n        return .init(api: api, properties: .init(api: api, snapshot: apiType))\n    }\n    \n    override func updateModel(_ item: Community, with apiType: AnyCommunitySnapshot, semaphore: UInt? = nil) {\n        // attempt a direct update through the queue to avoid overwriting more recent data, and also\n        // synchronously perform softUpdate to ensure high-tier data is available where expected\n        let properties: CommunityProperties = .init(api: item.api, snapshot: apiType)\n        Task {\n            await item.updateQueue.attemptDirectUpdate(with: properties)\n        }\n        item.softUpdate(with: properties)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/ImageUploadCaches.swift",
    "content": "//\n//  File.swift\n//\n//\n//  Created by Sjmarf on 26/08/2024.\n//\n\nimport Foundation\n\nclass ImageUpload1Cache: CoreCache<ImageUpload1> {\n    func getModel(api: ApiClient, from snapshot: ImageUpload1Snapshot, semaphore: UInt? = nil) -> ImageUpload1 {\n        if let item = retrieveModel(cacheId: snapshot.cacheId) { return item }\n        \n        let newItem: ImageUpload1 = .init(\n            api: api,\n            url: snapshot.url,\n            alias: snapshot.alias,\n            deleteToken: snapshot.deleteToken\n        )\n\n        itemCache.put(newItem)\n        return newItem\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/InstanceCache.swift",
    "content": "//\n//  InstanceCaches.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-01.\n//\n\nimport Foundation\n\npublic enum AnyInstanceSnapshot: CacheIdentifiable {\n    case instance1(Instance1Snapshot)\n    case instance2(Instance2Snapshot)\n    case instance3(Instance3Snapshot)\n    \n    public var cacheId: Int {\n        switch self {\n        case let .instance1(snapshot): snapshot.cacheId\n        case let .instance2(snapshot): snapshot.cacheId\n        case let .instance3(snapshot): snapshot.cacheId\n        }\n    }\n}\n\nclass InstanceCache: CoreCache<Instance> {\n    public var instanceIdCache: ItemCache = .init()\n    \n    @MainActor\n    func getModel(api: ApiClient, from snapshot: AnyInstanceSnapshot) -> Instance {\n        if let item = retrieveModel(cacheId: snapshot.cacheId) {\n            item.update(with: .init(api: api, snapshot: snapshot))\n            return item\n        }\n    \n        let newItem: Instance = .init(\n            api: api,\n            properties: .init(api: api, snapshot: snapshot)\n        )\n        \n        itemCache.put(newItem)\n        instanceIdCache.put(newItem, overrideCacheId: newItem.instanceId)\n        return newItem\n    }\n    \n    @MainActor\n    func getModels(api: ApiClient, from snapshots: [AnyInstanceSnapshot]) -> [Instance] {\n        snapshots.map { getModel(api: api, from: $0) }\n    }\n    \n    /// Get an instance with the given `instanceId` - this is different from the `id` of the instance.\n    public func retrieveModel(instanceId: Int) -> Instance? {\n        instanceIdCache.get(instanceId)\n    }\n    \n    override func clean() {\n        Task {\n            await itemCache.clean()\n            await instanceIdCache.clean()\n        }\n    }\n    \n    /// Convenience method for getting an optional site\n    @MainActor\n    func getOptionalModel(api: ApiClient, from snapshot: AnyInstanceSnapshot?) -> Instance? {\n        if let snapshot {\n            return getModel(api: api, from: snapshot)\n        }\n        return nil\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/MessageCaches.swift",
    "content": "//\n//  MessageCaches.swift\n//\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport Foundation\n\nclass Message1Cache: CoreCache<Message1> {\n    @MainActor\n    func getModel(\n        api: ApiClient,\n        from snapshot: Message1Snapshot,\n        myPersonId: Int,\n        semaphore: UInt? = nil\n    ) -> Message1 {\n        if let item = retrieveModel(cacheId: snapshot.cacheId) {\n            item.update(with: snapshot, semaphore: semaphore)\n            return item\n        }\n        \n        let newItem: Message1 = .init(\n            api: api,\n            actorId: snapshot.actorId,\n            id: snapshot.id,\n            creatorId: snapshot.creatorId,\n            recipientId: snapshot.recipientId,\n            isOwnMessage: myPersonId == snapshot.creatorId,\n            content: snapshot.content,\n            deleted: snapshot.deleted,\n            created: snapshot.created,\n            updated: snapshot.updated\n        )\n        itemCache.put(newItem)\n        return newItem\n    }\n    \n    @MainActor\n    func getModels(\n        api: ApiClient,\n        from snapshots: [Message1Snapshot],\n        myPersonId: Int,\n        semaphore: UInt? = nil\n    ) -> [Message1] {\n        snapshots.map { getModel(api: api, from: $0, myPersonId: myPersonId, semaphore: semaphore) }\n    }\n}\n\nclass Message2Cache: CoreCache<Message2> {\n    @MainActor\n    func getModel(\n        api: ApiClient,\n        from snapshot: Message2Snapshot,\n        myPersonId: Int,\n        semaphore: UInt? = nil\n    ) -> Message2 {\n        if let item = retrieveModel(cacheId: snapshot.cacheId) {\n            item.update(with: snapshot, semaphore: semaphore)\n            return item\n        }\n        \n        let newItem: Message2 = .init(\n            api: api,\n            message1: api.caches.message1.getModel(api: api, from: snapshot.message, myPersonId: myPersonId),\n            creator: api.caches.person.getModel(api: api, from: .person1(snapshot.creator)),\n            recipient: api.caches.person.getModel(api: api, from: .person1(snapshot.recipient))\n        )\n        itemCache.put(newItem)\n        return newItem\n    }\n    \n    @MainActor\n    func getModels(\n        api: ApiClient,\n        from snapshots: [Message2Snapshot],\n        myPersonId: Int,\n        semaphore: UInt? = nil\n    ) -> [Message2] {\n        snapshots.map { getModel(api: api, from: $0, myPersonId: myPersonId, semaphore: semaphore) }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/NotificationCaches.swift",
    "content": "//\n//  NotificationCaches.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-01.\n//\n\nimport Foundation\n\nclass NotificationCache: CoreCache<InboxNotification> {\n    @MainActor\n    func getModel(\n        api: ApiClient,\n        from snapshot: InboxNotificationSnapshot,\n        myPersonId: Int,\n        semaphore: UInt? = nil\n    ) -> InboxNotification {\n        if let item = retrieveModel(cacheId: snapshot.cacheId) {\n            Task {\n                await item.updateQueue.attemptDirectUpdate(with: snapshot)\n            }\n            return item\n        }\n\n        let content: InboxNotificationContent = switch snapshot.content {\n        case let .reply(commentSnapshot):\n                .reply(api.caches.comment.getModel(api: api, from: .comment2(commentSnapshot)))\n        case let .mention(commentSnapshot):\n                .mention(api.caches.comment.getModel(api: api, from: .comment2(commentSnapshot)))\n        case let .message(messageSnapshot):\n            .message(api.caches.message2.getModel(api: api, from: messageSnapshot, myPersonId: myPersonId))\n        }\n\n        let read: Bool = switch snapshot.content {\n        case let .message(messageSnapshot):\n            messageSnapshot.creator.id == myPersonId ? true : snapshot.read\n        default:\n            snapshot.read\n        }\n\n        let newItem: InboxNotification = .init(\n            api: api,\n            id: snapshot.id,\n            contentId: snapshot.contentId,\n            read: read,\n            content: content\n        )\n        itemCache.put(newItem)\n        return newItem\n    }\n\n    @MainActor\n    func getModels(\n        api: ApiClient,\n        from snapshots: [InboxNotificationSnapshot],\n        myPersonId: Int,\n        semaphore: UInt? = nil\n    ) -> [InboxNotification] {\n        snapshots.map { getModel(api: api, from: $0, myPersonId: myPersonId, semaphore: semaphore) }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/PersonCache.swift",
    "content": "//\n//  PersonCache.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-01.\n//\n\nimport Foundation\n\npublic enum AnyPersonSnapshot: CacheIdentifiable {\n    case person1(Person1Snapshot)\n    case person2(Person2Snapshot)\n    case person3(Person3Snapshot)\n    case person4(Person4Snapshot)\n    \n    static func person1(_ snapshot: Person1Snapshot?) -> AnyPersonSnapshot? {\n        if let snapshot {\n            return .person1(snapshot)\n        }\n        return nil\n    }\n    \n    static func person2(_ snapshot: Person2Snapshot?) -> AnyPersonSnapshot? {\n        if let snapshot {\n            return .person2(snapshot)\n        }\n        return nil\n    }\n    \n    static func person3(_ snapshot: Person3Snapshot?) -> AnyPersonSnapshot? {\n        if let snapshot {\n            return .person3(snapshot)\n        }\n        return nil\n    }\n    \n    static func person4(_ snapshot: Person4Snapshot?) -> AnyPersonSnapshot? {\n        if let snapshot {\n            return .person4(snapshot)\n        }\n        return nil\n    }\n    \n    public var cacheId: Int {\n        switch self {\n        case let .person1(snapshot): snapshot.cacheId\n        case let .person2(snapshot): snapshot.cacheId\n        case let .person3(snapshot): snapshot.cacheId\n        case let .person4(snapshot): snapshot.cacheId\n        }\n    }\n}\n\nclass PersonCache: ApiTypeBackedCache<Person, AnyPersonSnapshot> {\n    override func performModelTranslation(api: ApiClient, from apiType: AnyPersonSnapshot) -> Person {\n        return .init(api: api, properties: .init(api: api, snapshot: apiType))\n    }\n    \n    override func updateModel(_ item: Person, with apiType: AnyPersonSnapshot, semaphore: UInt? = nil) {\n        // attempt a direct update through the queue to avoid overwriting more recent data, and also\n        // synchronously perform softUpdate to ensure high-tier data is available where expected\n        let properties: PersonProperties = .init(api: item.api, snapshot: apiType)\n        Task {\n            await item.updateQueue.attemptDirectUpdate(with: properties)\n        }\n        item.softUpdate(with: properties)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/PersonVoteCaches.swift",
    "content": "//\n//  PersonVoteCaches.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-18.\n//\n\nimport Foundation\n\nclass PersonVoteCache: CoreCache<PersonVote> {\n    @MainActor\n    func getModel(\n        api: ApiClient,\n        from snapshot: PersonVoteSnapshot,\n        target: PersonVote.Target,\n        communityId: Int,\n        semaphore: UInt? = nil\n    ) -> PersonVote {\n        if let item = retrieveModel(cacheId: getCacheId(target: target, creatorId: snapshot.creator.id)) {\n            item.update(with: snapshot, semaphore: semaphore)\n            return item\n        }\n        \n        let newItem: PersonVote = .init(\n            api: api,\n            target: target,\n            communityId: communityId,\n            creator: api.caches.person.getModel(api: api, from: .person1(snapshot.creator)),\n            vote: .init(rawValue: snapshot.score) ?? .none,\n            creatorBannedFromCommunity: snapshot.creatorBannedFromCommunity\n        )\n        itemCache.put(newItem)\n        return newItem\n    }\n    \n    @MainActor\n    func getModels(\n        api: ApiClient,\n        from snapshots: [PersonVoteSnapshot],\n        target: PersonVote.Target,\n        communityId: Int,\n        semaphore: UInt? = nil\n    ) -> [PersonVote] {\n        snapshots.map {\n            getModel(\n                api: api,\n                from: $0,\n                target: target,\n                communityId: communityId,\n                semaphore: semaphore\n            )\n        }\n    }\n    \n    private func getCacheId(target: PersonVote.Target, creatorId: Int) -> Int {\n        var hasher = Hasher()\n        hasher.combine(target)\n        hasher.combine(creatorId)\n        return hasher.finalize()\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/PostCache.swift",
    "content": "//\n//  PostCache.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-03.\n//\n\npublic enum AnyPostSnapshot: CacheIdentifiable {\n    case post1(Post1Snapshot)\n    case post2(Post2Snapshot)\n    case post3(Post3Snapshot)\n    \n    public var cacheId: Int {\n        switch self {\n        case let .post1(snapshot): snapshot.cacheId\n        case let .post2(snapshot): snapshot.cacheId\n        case let .post3(snapshot): snapshot.cacheId\n        }\n    }\n}\n\nclass PostCache: ApiTypeBackedCache<Post, AnyPostSnapshot> {\n    override func performModelTranslation(api: ApiClient, from apiType: AnyPostSnapshot) -> Post {\n        return .init(api: api, properties: .init(api: api, snapshot: apiType))\n    }\n    \n    override func updateModel(_ item: Post, with apiType: AnyPostSnapshot, semaphore: UInt? = nil) {\n        // attempt a direct update through the queue to avoid overwriting more recent data, and also\n        // synchronously perform softUpdate to ensure high-tier data is available where expected\n        let properties: PostProperties = .init(api: item.api, snapshot: apiType)\n        Task {\n            await item.updateQueue.attemptDirectUpdate(with: properties)\n        }\n        item.softUpdate(with: properties)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/RegistrationApplicationCaches.swift",
    "content": "//\n//  RegistrationApplicationCaches.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-01-12.\n//\n\nimport Foundation\n\nclass RegistrationApplicationCache: ApiTypeBackedCache<RegistrationApplication, RegistrationApplicationSnapshot> {\n    @MainActor\n    override func performModelTranslation(\n        api: ApiClient,\n        from snapshot: RegistrationApplicationSnapshot\n    ) -> RegistrationApplication {\n        .init(\n            api: api,\n            id: snapshot.id,\n            questionResponse: snapshot.questionResponse,\n            creator: api.caches.person.getModel(api: api, from: .person1(snapshot.creator)),\n            resolver: api.caches.person.getOptionalModel(api: api, from: .person1(snapshot.resolver)),\n            email: snapshot.email,\n            emailVerified: snapshot.emailVerified,\n            showNsfw: snapshot.showNsfw,\n            created: snapshot.created,\n            resolution: snapshot.resolution\n        )\n    }\n    \n    @MainActor\n    override func updateModel(\n        _ item: RegistrationApplication,\n        with snapshot: RegistrationApplicationSnapshot,\n        semaphore: UInt? = nil\n    ) {\n        item.update(with: snapshot, semaphore: semaphore)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Caches/ReportCaches.swift",
    "content": "//\n//  ReportCaches.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-16.\n//\n\nimport Foundation\n\n// Report can be created from any ReportApiBacker, so we can't use ApiTypeBackedCache\nclass ReportCache: CoreCache<Report> {\n    @MainActor\n    func getModel(\n        api: ApiClient,\n        from snapshot: ReportSnapshot,\n        myPersonId: Int,\n        semaphore: UInt? = nil\n    ) -> Report {\n        if let item = retrieveModel(cacheId: snapshot.cacheId) {\n            item.update(with: snapshot, semaphore: semaphore)\n            return item\n        }\n        \n        let newItem: Report = .init(\n            api: api,\n            id: snapshot.id,\n            creator: api.caches.person.getModel(api: api, from: .person1(snapshot.creator), semaphore: semaphore),\n            resolver: api.caches.person.getOptionalModel(api: api, from: .person1(snapshot.resolver), semaphore: semaphore),\n            target: .init(from: snapshot.target, api: api, myPersonId: myPersonId),\n            resolved: snapshot.resolved,\n            reason: snapshot.reason,\n            created: snapshot.created,\n            updated: snapshot.updated\n        )\n        itemCache.put(newItem)\n        return newItem\n    }\n    \n    @MainActor\n    func getModels(\n        api: ApiClient,\n        from snapshots: [ReportSnapshot],\n        myPersonId: Int,\n        semaphore: UInt? = nil\n    ) -> [Report] {\n        snapshots.map { getModel(api: api, from: $0, myPersonId: myPersonId, semaphore: semaphore) }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Conformance/ImageUpload+CacheExtensions.swift",
    "content": "//\n//  ImageUpload+CacheExtensions.swift\n//\n//\n//  Created by Sjmarf on 26/08/2024.\n//\n\nimport Foundation\n\nextension ImageUpload1: CacheIdentifiable {\n    public var cacheId: Int { alias.hashValue }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Conformance/InboxNotification+CacheExtensions.swift",
    "content": "//\n//  Notification+CacheExtensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-02.\n//\n\nextension InboxNotification: CacheIdentifiable {\n    public var cacheId: Int { id }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Conformance/Message+CacheExtensions.swift",
    "content": "//\n//  Message+CacheExtensions.swift\n//\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport Foundation\n\nextension Message1: CacheIdentifiable {\n    public var cacheId: Int { id }\n    \n    @MainActor\n    func update(with snapshot: Message1Snapshot, semaphore: UInt? = nil) {\n        setIfChanged(\\.content, snapshot.content)\n        setIfChanged(\\.updated, snapshot.updated)\n        \n        deletedManager.updateWithReceivedValue(snapshot.deleted, semaphore: semaphore)\n    }\n}\n\nextension Message2: CacheIdentifiable {\n    public var cacheId: Int { id }\n    \n    @MainActor\n    func update(with snapshot: Message2Snapshot, semaphore: UInt? = nil) {\n        message1.update(with: snapshot.message, semaphore: semaphore)\n        \n        Task {\n            await creator.updateQueue.attemptDirectUpdate(with: .init(api: api, snapshot: .person1(snapshot.creator)))\n            await recipient.updateQueue.attemptDirectUpdate(with: .init(api: api, snapshot: .person1(snapshot.recipient)))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Conformance/RegistrationApplication+CacheExtensions.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-01-12.\n//\n\nimport Foundation\n\nextension RegistrationApplication: CacheIdentifiable {\n    public var cacheId: Int { id }\n    \n    @MainActor\n    func update(with snapshot: RegistrationApplicationSnapshot, semaphore: UInt? = nil) {\n        setIfChanged(\\.questionResponse, snapshot.questionResponse)\n        resolutionManager.updateWithReceivedValue(resolution, semaphore: semaphore)\n        setIfChanged(\\.resolver, api.caches.person.getOptionalModel(api: api, from: .person1(snapshot.resolver)))\n        setIfChanged(\\.email, snapshot.email)\n        setIfChanged(\\.emailVerified, snapshot.emailVerified)\n        setIfChanged(\\.showNsfw, snapshot.showNsfw)\n        \n        Task {\n            await creator.updateQueue.attemptDirectUpdate(with: .init(api: api, snapshot: .person1(snapshot.creator)))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/Conformance/Report+CacheExtensions.swift",
    "content": "//\n//  Report.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-16.\n//\n\nimport Foundation\n\nextension Report {\n    public var cacheId: Int {\n        var hasher = Hasher()\n        hasher.combine(target.type)\n        hasher.combine(id)\n        return hasher.finalize()\n    }\n    \n    @MainActor\n    func update(with snapshot: ReportSnapshot, semaphore: UInt? = nil) {\n        setIfChanged(\\.updated, snapshot.updated)\n        setIfChanged(\\.reason, snapshot.reason)\n        setIfChanged(\\.resolver, api.caches.person.getOptionalModel(api: api, from: .person1(snapshot.resolver)))\n        resolvedManager.updateWithReceivedValue(snapshot.resolved, semaphore: semaphore)\n        \n        target.update(with: snapshot.target)\n        \n        Task {\n            await creator.updateQueue.attemptDirectUpdate(with: .init(api: api, snapshot: .person1(snapshot.creator)))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Caching/CoreCache.swift",
    "content": "//\n//  CoreCache.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-01.\n//\n\nimport Foundation\nimport MlemLogger\nimport os\nimport Semaphore\n\n/// Class providing common caching behavior\nopen class CoreCache<Content: CacheIdentifiable & AnyObject> {\n    public var itemCache: ItemCache = .init()\n    \n    public init() {\n        self.itemCache = .init()\n    }\n    \n    public class ItemCache {\n        private let log: Logger = .mlemLogger()\n        \n        private var cachedItems: Atomic<[Int: WeakReference<Content>]> = .init(.init())\n        private let cleaningSemaphore: AsyncSemaphore = .init(value: 1)\n        \n        var value: [Int: WeakReference<Content>] {\n            cachedItems.value\n        }\n        \n        public func put(_ item: Content, overrideCacheId: Int? = nil) {\n            let cacheId = overrideCacheId ?? item.cacheId\n            cachedItems.value[cacheId] = .init(content: item)\n        }\n        \n        public func get(_ cacheId: Int) -> Content? {\n            cachedItems.value[cacheId]?.content\n        }\n        \n        public func remove(_ cacheId: Int) {\n            log.debug(\"Removed \\(cacheId)\")\n            cachedItems.value[cacheId] = nil\n        }\n        \n        public func clean() async {\n            await cleaningSemaphore.wait()\n            defer { cleaningSemaphore.signal() }\n            for (key, value) in cachedItems.value where value.content == nil {\n                remove(key)\n            }\n        }\n    }\n    \n    /// Retrieves the cached model with the given cacheId, if present\n    /// - Parameter cacheId: cacheId of the model to retrieve\n    /// - Returns: cached model if present, nil otherwise\n    public func retrieveModel(cacheId: Int) -> Content? {\n        itemCache.get(cacheId)\n    }\n    \n    /// Remove dead references\n    public func clean() {\n        Task {\n            await itemCache.clean()\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Helpers/MarkReadQueue.swift",
    "content": "//\n//  MarkReadQueue.swift\n//\n//\n//  Created by Sjmarf on 29/05/2024.\n//\n\nimport Foundation\n\nactor MarkReadQueue {\n    var ids: Set<Int> = .init()\n    \n    func popAll() -> Set<Int> {\n        defer { ids.removeAll() }\n        return ids\n    }\n    \n    func add(_ postId: Int) {\n        ids.insert(postId)\n    }\n    \n    func remove(_ postId: Int) {\n        ids.remove(postId)\n    }\n    \n    func union(_ other: Set<Int>) {\n        ids.formUnion(other)\n    }\n    \n    func subtract(_ other: Set<Int>) {\n        ids.subtract(other)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Client/Helpers/SharedTaskManager.swift",
    "content": "//\n//  SharedTaskManager.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-27.\n//\n\nimport Foundation\n\npublic class SharedTaskManager<Value, TaskResponse> {\n    var fetchTask: (() async throws -> TaskResponse)!\n    var createValue: ((TaskResponse) -> Value)!\n    \n    var ongoingTask: Task<TaskResponse, Error>?\n    var fetchedValue: Value?\n    \n    init(\n        fetchTask: (() async throws -> TaskResponse)? = nil,\n        createValue: ((TaskResponse) -> Value)? = nil\n    ) {\n        self.fetchTask = fetchTask\n        self.createValue = createValue\n    }\n    \n    @discardableResult\n    public func getValue(task: Task<TaskResponse, Error>? = nil) async throws -> Value {\n        if let fetchedValue {\n            return fetchedValue\n        } else {\n            if let ongoingTask {\n                let result = await ongoingTask.result\n                return try createValue(result.get())\n            } else {\n                defer { ongoingTask = nil }\n                let task = task ?? ongoingTask ?? Task { try await fetchTask() }\n                ongoingTask = task\n                let result = await task.result\n                fetchedValue = try createValue(result.get())\n                return try await createValue(fetchTask())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+Comment.swift",
    "content": "//\n//  ApiRepository+Comment.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-02.\n//\n\nimport Foundation\n\nextension ApiRepository {\n    func getComment(id: Int) async throws -> Comment2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.getComment(id: id)\n        }\n    }\n    \n    func getComment(url: URL) async throws -> Comment2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.getComment(url: url)\n        }\n    }\n    \n    func getComments(\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.getComments(\n                sort: sort,\n                page: page,\n                maxDepth: maxDepth,\n                limit: limit,\n                filter: filter\n            )\n        }\n    }\n    \n    func getComments(\n        postId: Int,\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.getComments(\n                postId: postId,\n                sort: sort,\n                page: page,\n                maxDepth: maxDepth,\n                limit: limit,\n                filter: filter\n            )\n        }\n    }\n    \n    func getComments(\n        parentId: Int,\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.getComments(\n                parentId: parentId,\n                sort: sort,\n                page: page,\n                maxDepth: maxDepth,\n                limit: limit,\n                filter: filter\n            )\n        }\n    }\n\n    func getCommentHistory(\n        type: GetContentFilter,\n        page: Int?,\n        cursor: String?,\n        limit: Int\n    ) async throws -> (comments: [Comment2Snapshot], cursor: String?) {\n        try await performingForConnection { connection in\n            try await connection.getCommentHistory(\n                type: type,\n                page: page,\n                cursor: cursor,\n                limit: limit\n            )\n        }\n    }\n    \n    // TODO: Remove in favor of the below method once we drop support for versions before Lemmy 1.0\n    func searchComments(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: CommentSortType = .top(.allTime)\n    ) async throws -> [Comment2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.searchComments(\n                query: query,\n                page: page,\n                limit: limit,\n                communityId: communityId,\n                creatorId: creatorId,\n                filter: filter,\n                sort: sort\n            )\n        }\n    }\n    \n    func searchComments(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) async throws -> [Comment2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.searchComments(\n                query: query,\n                page: page,\n                limit: limit,\n                communityId: communityId,\n                creatorId: creatorId,\n                filter: filter,\n                sort: sort\n            )\n        }\n    }\n    \n    func voteOnComment(id: Int, score: ScoringOperation, semaphore: UInt? = nil) async throws -> Comment2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.voteOnComment(id: id, score: score)\n        }\n    }\n    \n    func saveComment(id: Int, save: Bool, semaphore: UInt? = nil) async throws -> Comment2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.saveComment(id: id, save: save)\n        }\n    }\n    \n    func deleteComment(id: Int, delete: Bool, semaphore: UInt? = nil) async throws -> Comment2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.deleteComment(id: id, delete: delete)\n        }\n    }\n    \n    func editComment(\n        id: Int,\n        content: String,\n        languageId: Int?\n    ) async throws -> Comment2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.editComment(\n                id: id,\n                content: content,\n                languageId: languageId\n            )\n        }\n    }\n    \n    // There's also a `replyToPost` method in `ApiRepository+Post` for creating a comment on a post\n    func replyToComment(postId: Int, parentId: Int?, content: String, languageId: Int? = nil) async throws -> Comment2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.replyToComment(\n                postId: postId,\n                parentId: parentId,\n                content: content,\n                languageId: languageId\n            )\n        }\n    }\n    \n    func reportComment(id: Int, reason: String) async throws -> ReportSnapshot {\n        try await performingForConnection { connection in\n            try await connection.reportComment(id: id, reason: reason)\n        }\n    }\n    \n    func purgeComment(id: Int, reason: String?) async throws {\n        try await performingForConnection { connection in\n            try await connection.purgeComment(id: id, reason: reason)\n        }\n    }\n    \n    func removeComment(\n        id: Int,\n        remove: Bool,\n        reason: String?,\n        semaphore: UInt? = nil\n    ) async throws -> Comment2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.removeComment(id: id, remove: remove, reason: reason)\n        }\n    }\n    \n    func getCommentVotes(\n        id: Int,\n        page: Int = 1,\n        limit: Int = 20\n    ) async throws -> [PersonVoteSnapshot] {\n        try await performingForConnection { connection in\n            try await connection.getCommentVotes(\n                id: id,\n                page: page,\n                limit: limit\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+Community.swift",
    "content": "//\n//  ApiRepository+Community.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-02.\n//\n\nimport Foundation\n\nextension ApiRepository {\n    func getCommunity(id: Int) async throws -> Community3Snapshot {\n        try await performingForConnection { connection in\n            try await connection.getCommunity(id: id)\n        }\n    }\n    \n    func getCommunity(url: URL) async throws -> Community2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.getCommunity(url: url)\n        }\n    }\n    \n    func searchCommunities(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        filter: ListingType = .all,\n        sort: SearchSortType,\n        hostApi: ApiClient? = nil\n    ) async throws -> [Community2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.searchCommunities(\n                query: query,\n                page: page,\n                limit: limit,\n                filter: filter,\n                sort: sort\n            )\n        }\n    }\n\n    func editCommunityDescription(id: Int, newValue: String?) async throws -> Community2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.editCommunityDescription(id: id, newValue: newValue)\n        }\n    }\n    \n    func getSubscriptionList(page: Int, limit: Int) async throws -> [Community2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.getSubscriptionList(page: page, limit: limit)\n        }\n    }\n    \n    func subscribeToCommunity(id: Int, subscribe: Bool) async throws -> Community2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.subscribeToCommunity(id: id, subscribe: subscribe)\n        }\n    }\n    \n    func blockCommunity(id: Int, block: Bool, semaphore: UInt? = nil) async throws -> Community2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.blockCommunity(id: id, block: block)\n        }\n    }\n    \n    func removeCommunity(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Community2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.removeCommunity(\n                id: id,\n                remove: remove,\n                reason: reason\n            )\n        }\n    }\n    \n    func purgeCommunity(id: Int, reason: String?) async throws {\n        try await performingForConnection { connection in\n            try await connection.purgeCommunity(id: id, reason: reason)\n        }\n    }\n    \n    func addModerator(communityId: Int, personId: Int, added: Bool) async throws -> (moderators: [Person1Snapshot], community: Community1Snapshot) {\n        try await performingForConnection { connection in\n            try await connection.addModerator(\n                communityId: communityId,\n                personId: personId,\n                added: added\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+General.swift",
    "content": "//\n//  ApiRepository+General.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-02.\n//\n\nimport Foundation\n\nextension ApiRepository {\n    func getAccountToken(usernameOrEmail: String, password: String, totpToken: String?) async throws -> String {\n        try await performingForConnection { connection in\n            try await connection.getAccountToken(\n                usernameOrEmail: usernameOrEmail,\n                password: password,\n                totpToken: totpToken\n            )\n        }\n    }\n    \n    func getUsernameFromToken(token: String) async throws -> String {\n        try await performingForConnection { connection in\n            try await connection.getUsernameFromToken(token: token)\n        }\n    }\n    \n    func signUp(\n        username: String,\n        password: String,\n        confirmPassword: String,\n        showNsfw: Bool,\n        email: String?,\n        captcha: Captcha?,\n        captchaAnswer: String?,\n        applicationQuestionResponse: String?\n    ) async throws -> SignUpResponse {\n        try await performingForConnection { connection in\n            try await connection.signUp(\n                username: username,\n                password: password,\n                confirmPassword: confirmPassword,\n                showNsfw: showNsfw,\n                email: email,\n                captcha: captcha,\n                captchaAnswer: captchaAnswer,\n                applicationQuestionResponse: applicationQuestionResponse\n            )\n        }\n    }\n    \n    func changePassword(\n        newPassword: String,\n        confirmNewPassword: String,\n        oldPassword: String\n    ) async throws -> String {\n        try await performingForConnection { connection in\n            try await connection.changePassword(\n                newPassword: newPassword,\n                confirmNewPassword: confirmNewPassword,\n                oldPassword: oldPassword\n            )\n        }\n    }\n    \n    func getCaptcha() async throws -> Captcha {\n        try await performingForConnection { connection in\n            try await connection.getCaptcha()\n        }\n    }\n    \n    func resolve(url: URL) async throws -> ResolvedContent {\n        try await performingForConnection { connection in\n            try await connection.resolve(url: url)\n        }\n    }\n    \n    func getBlocked() async throws -> (people: [Person1Snapshot], communities: [Community1Snapshot], instances: [Instance1Snapshot]) {\n        try await performingForConnection { connection in\n            try await connection.getBlocked()\n        }\n    }\n    \n    func getModlog(\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        moderatorId: Int? = nil,\n        subjectPersonId: Int? = nil,\n        postId: Int? = nil,\n        commentId: Int? = nil,\n        type: ModlogEntryType? = nil\n    ) async throws -> [ModlogEntrySnapshot] {\n        try await performingForConnection { connection in\n            try await connection.getModlog(\n                page: page,\n                limit: limit,\n                communityId: communityId,\n                moderatorId: moderatorId,\n                subjectPersonId: subjectPersonId,\n                postId: postId,\n                commentId: commentId,\n                type: type\n            )\n        }\n    }\n    \n    func getPostLink(url: URL) async throws -> PostLink {\n        try await performingForConnection { connection in\n            try await connection.getPostLink(url: url)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+Image.swift",
    "content": "//\n//  ApiRepository+Image.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-03.\n//\n\nimport Foundation\nimport Rest\n\nextension ApiRepository {\n    func uploadImage(\n        _ imageData: Data,\n        fileExtension: String,\n        onProgress progressCallback: @escaping (_ progress: Double) -> Void = { _ in }\n    ) async throws -> ImageUpload1Snapshot {\n        try await performingForConnection { connection in\n            try await connection.uploadImage(imageData, fileExtension: fileExtension, onProgress: progressCallback)\n        }\n    }\n    \n    func deleteImage(alias: String, deleteToken: String) async throws {\n        try await performingForConnection { connection in\n            try await connection.deleteImage(alias: alias, deleteToken: deleteToken)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+Inbox.swift",
    "content": "//\n//  ApiRepository+Inbox.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-03.\n//\n\nextension ApiRepository {\n    func getMessages(\n        creatorId: Int? = nil,\n        page: Int,\n        limit: Int,\n        unreadOnly: Bool = false\n    ) async throws -> [Message2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.getMessages(\n                creatorId: creatorId,\n                page: page,\n                limit: limit,\n                unreadOnly: unreadOnly\n            )\n        }\n    }\n\n    func getReplyNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?) {\n        try await performingForConnection { connection in\n            try await connection.getReplyNotifications(\n                page: page,\n                cursor: cursor,\n                limit: limit,\n                unreadOnly: unreadOnly\n            )\n        }\n    }\n    \n    func getMentionNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?) {\n        try await performingForConnection { connection in\n            try await connection.getMentionNotifications(\n                page: page,\n                cursor: cursor,\n                limit: limit,\n                unreadOnly: unreadOnly\n            )\n        }\n    }\n\n    func getMessageNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?) {\n        try await performingForConnection { connection in\n            try await connection.getMessageNotifications(\n                page: page,\n                cursor: cursor,\n                limit: limit,\n                unreadOnly: unreadOnly\n            )\n        }\n    }\n    \n    func markNotificationAsRead(\n        type: InboxNotificationContentType,\n        id: Int,\n        contentId: Int,\n        read: Bool\n    ) async throws {\n        try await performingForConnection { connection in\n            try await connection.markNotificationAsRead(\n                type: type,\n                id: id,\n                contentId: contentId,\n                read: read\n            )\n        }\n    }\n\n    func markAllAsRead() async throws {\n        try await performingForConnection { connection in\n            try await connection.markAllAsRead()\n        }\n    }\n    \n    func getPersonalUnreadCount() async throws -> PersonalUnreadCountSnapshot {\n        try await performingForConnection { connection in\n            try await connection.getPersonalUnreadCount()\n        }\n    }\n    \n    func createMessage(personId: Int, content: String) async throws -> Message2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.createMessage(personId: personId, content: content)\n        }\n    }\n    \n    func editMessage(id: Int, content: String) async throws -> Message2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.editMessage(id: id, content: content)\n        }\n    }\n    \n    func reportMessage(id: Int, reason: String) async throws -> ReportSnapshot {\n        try await performingForConnection { connection in\n            try await connection.reportMessage(id: id, reason: reason)\n        }\n    }\n    \n    func deleteMessage(id: Int, delete: Bool, semaphore: UInt? = nil) async throws -> Message2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.deleteMessage(id: id, delete: delete)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+Instance.swift",
    "content": "//\n//  ApiRepository+Instance.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-02.\n//\n\nextension ApiRepository {\n    func getMyInstance() async throws -> Instance3Snapshot {\n        return try await performingForConnection { connection in\n            try await connection.getMyInstance()\n        }\n    }\n    \n    func getFederatedInstances() async throws -> FederationPolicy {\n        let response = try await performingForConnection { connection in\n            try await connection.getFederatedInstances()\n        }\n        return response\n    }\n    \n    func blockInstance(instanceId: Int, block: Bool) async throws {\n        try await performingForConnection { connection in\n            try await connection.blockInstance(instanceId: instanceId, block: block)\n        }\n    }\n    \n    func addAdmin(personId: Int, added: Bool) async throws -> [Person2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.addAdmin(personId: personId, added: added)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+Mock.swift",
    "content": "//\n//  ApiRepository+Mock.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-03.\n//\n\nimport Foundation\nimport Rest\n\n// TODO: updated mocks\n//#if DEBUG\n//\n//    class MockApiRepository: ApiRepository {\n//        var posts: [Post2]\n//        var communities: [Community2]\n//        var people: [Person2]\n//        var comments: [Comment2]\n//    \n//        init(\n//            url: URL,\n//            username: String,\n//            posts: [Post2] = [],\n//            communities: [Community2] = [],\n//            people: [Person2] = [],\n//            comments: [Comment2] = []\n//        ) {\n//            self.posts = posts\n//            self.communities = communities\n//            self.people = people\n//            self.comments = comments\n//        \n//            super.init(\n//                baseUrl: url,\n//                username: username\n//            )\n//            self.token = \"\" // Not nil so that the views are interactable\n//            let connection = LemmyConnection(baseUrl: url, token: \"\")\n//            connection.setMockContext(.init(siteVersion: .init(\"0.19.0\"), myPersonId: nil))\n//            self.connection = connection\n//        }\n//    \n//        override func perform<Request: RestRequest>(\n//            _ request: Request,\n//            tokenOverride: String? = nil,\n//            requiresToken: Bool = true\n//        ) async throws -> Request.Response {\n//            if let request = request as? LemmyListPostsRequest, request.parameters != nil {\n//                return LemmyGetPostsResponse(\n//                    posts: posts.map(\\.apiPostView),\n//                    nextPage: nil\n//                ) as! Request.Response\n//            }\n//    \n//            if let request = request as? LemmyListCommentsRequest, request.parameters != nil {\n//                return LemmyGetCommentsResponse(comments: comments.map(\\.apiCommentView)) as! Request.Response\n//            }\n//    \n//            if let request = request as? LemmyReadPersonRequest, let params = request.parameters {\n//                if let person = people.first(where: { $0.id == params.personId })?.apiPersonView {\n//                    return LemmyGetPersonDetailsResponse(\n//                        personView: person,\n//                        comments: nil,\n//                        posts: posts.filter { $0.creator.id == params.personId }.map(\\.apiPostView),\n//                        moderates: [],\n//                        site: nil,\n//                        multiCommunitiesCreated: nil\n//                    ) as! Request.Response\n//                }\n//            }\n//    \n//            if let request = request as? LemmyResolveObjectRequest, let params = request.parameters {\n//                return LemmyResolveObjectResponse(\n//                    comment: comments.first(where: { $0.actorId.description == params.q })?.apiCommentView,\n//                    post: posts.first(where: { $0.actorId.description == params.q })?.apiPostView,\n//                    community: communities.first(where: { $0.actorId.description == params.q })?.apiCommunityView,\n//                    person: people.first(where: { $0.actorId.description == params.q })?.apiPersonView\n//                ) as! Request.Response\n//            }\n//    \n//            if let request = request as? LemmyGetCommunityRequest, let params = request.parameters {\n//                if let community = communities.first(where: { $0.id == params.id })?.apiCommunityView {\n//                    return LemmyGetCommunityResponse(\n//                        communityView: community,\n//                        site: nil,\n//                        moderators: [],\n//                        discussionLanguages: []\n//                    ) as! Request.Response\n//                }\n//            }\n//    \n//            if let request = request as? LemmySearchRequest, let params = request.parameters {\n//                return LemmySearchResponse(\n//                    type_: params.type_,\n//                    comments: [],\n//                    posts: [],\n//                    communities: params.type_ == .communities ? communities.map(\\.apiCommunityView) : [],\n//                    users: params.type_ == .users ? people.map(\\.apiPersonView) : [],\n//                    resolve: nil,\n//                    search: nil,\n//                    nextPage: nil,\n//                    prevPage: nil\n//                ) as! Request.Response\n//            }\n//    \n//            throw ApiClientError.insufficientPermissions\n//        }\n//    }\n//\n//#endif\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+Person.swift",
    "content": "//\n//  ApiRepository+Person.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-03.\n//\n\nimport Foundation\n\nextension ApiRepository {\n    func getPerson(id: Int) async throws -> Person3Snapshot {\n        try await performingForConnection { connection in\n            try await connection.getPerson(id: id)\n        }\n    }\n    \n    func getPerson(url: URL) async throws -> Person2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.getPerson(url: url)\n        }\n    }\n    \n    func getPerson(username: String) async throws -> Person3Snapshot {\n        try await performingForConnection { connection in\n            try await connection.getPerson(username: username)\n        }\n    }\n    \n    /// `filter` can be set to `.local` from 0.19.4 onwards.\n    func searchPeople(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        filter: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) async throws -> [Person2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.searchPeople(\n                query: query,\n                page: page,\n                limit: limit,\n                filter: filter,\n                sort: sort\n            )\n        }\n    }\n    \n    func blockPerson(id: Int, block: Bool, semaphore: UInt? = nil) async throws -> Person2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.blockPerson(id: id, block: block)\n        }\n    }\n    \n    func banPersonFromCommunity(\n        personId: Int,\n        communityId: Int,\n        ban: Bool,\n        removeContent: Bool,\n        reason: String?,\n        expires: Date? = nil\n    ) async throws -> Person1Snapshot {\n        try await performingForConnection { connection in\n            try await connection.banPersonFromCommunity(\n                personId: personId,\n                communityId: communityId,\n                ban: ban,\n                removeContent: removeContent,\n                reason: reason,\n                expires: expires\n            )\n        }\n    }\n    \n    func banPersonFromInstance(\n        personId: Int,\n        ban: Bool,\n        removeContent: Bool,\n        reason: String?,\n        expires: Date? = nil\n    ) async throws -> Person2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.banPersonFromInstance(\n                personId: personId,\n                ban: ban,\n                removeContent: removeContent,\n                reason: reason,\n                expires: expires\n            )\n        }\n    }\n    \n    func purgePerson(id: Int, reason: String?) async throws {\n        try await performingForConnection { connection in\n            try await connection.purgePerson(id: id, reason: reason)\n        }\n    }\n    \n    func getContent(\n        authorId id: Int,\n        sort: PostSortType,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool? = nil,\n        communityId: Int? = nil\n    ) async throws -> (person: Person3Snapshot, posts: [Post2Snapshot], comments: [Comment2Snapshot]) {\n        try await performingForConnection { connection in\n            try await connection.getContent(\n                authorId: id,\n                sort: sort,\n                page: page,\n                limit: limit,\n                savedOnly: savedOnly,\n                communityId: communityId\n            )\n        }\n    }\n    \n    func getMyPerson() async throws -> (person: Person4Snapshot?, instance: Instance3Snapshot, blocks: BlockListSnapshot?) {\n        try await performingForConnection { connection in\n            try await connection.getMyPerson()\n        }\n    }\n    \n    func deleteAccount(password: String, deleteContent: Bool) async throws {\n        try await performingForConnection { connection in\n            try await connection.deleteAccount(password: password, deleteContent: deleteContent)\n        }\n    }\n\n    func editNote(id: Int, content: String?) async throws {\n        try await performingForConnection { connection in\n            try await connection.editNote(id: id, content: content)\n        }\n    }\n\n    func editProfile(_ details: ProfileDetails) async throws {\n        try await performingForConnection { connection in\n            try await connection.editProfile(details: details)\n        }\n    }\n    \n    func editAccountSettings(\n        showNsfw: Bool?,\n        showScores: Bool?,\n        theme: String?,\n        defaultListingType: ListingType?,\n        interfaceLanguage: String?,\n        avatar: String?,\n        banner: String?,\n        displayName: String?,\n        email: String?,\n        bio: String?,\n        matrixUserId: String?,\n        showAvatars: Bool?,\n        sendNotificationsToEmail: Bool?,\n        botAccount: Bool?,\n        showBotAccounts: Bool?,\n        showReadPosts: Bool?,\n        discussionLanguages: [Int]?,\n        openLinksInNewTab: Bool?,\n        blurNsfw: Bool?,\n        autoExpand: Bool?,\n        infiniteScrollEnabled: Bool?,\n        postListingMode: PostFeedViewMode?,\n        enableKeyboardNavigation: Bool?,\n        enableAnimatedImages: Bool?,\n        collapseBotComments: Bool?,\n        showUpvotes: Bool?,\n        showDownvotes: Bool?,\n        showUpvotePercentage: Bool?\n    ) async throws {\n        try await performingForConnection { connection in\n            try await connection.editAccountSettings(\n                showNsfw: showNsfw,\n                showScores: showScores,\n                theme: theme,\n                defaultListingType: defaultListingType,\n                interfaceLanguage: interfaceLanguage,\n                avatar: avatar,\n                banner: banner,\n                displayName: displayName,\n                email: email,\n                bio: bio,\n                matrixUserId: matrixUserId,\n                showAvatars: showAvatars,\n                sendNotificationsToEmail: sendNotificationsToEmail,\n                botAccount: botAccount,\n                showBotAccounts: showBotAccounts,\n                showReadPosts: showReadPosts,\n                discussionLanguages: discussionLanguages,\n                openLinksInNewTab: openLinksInNewTab,\n                blurNsfw: blurNsfw,\n                autoExpand: autoExpand,\n                infiniteScrollEnabled: infiniteScrollEnabled,\n                postListingMode: postListingMode,\n                enableKeyboardNavigation: enableKeyboardNavigation,\n                enableAnimatedImages: enableAnimatedImages,\n                collapseBotComments: collapseBotComments,\n                showUpvotes: showUpvotes,\n                showDownvotes: showDownvotes,\n                showUpvotePercentage: showUpvotePercentage\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+Post.swift",
    "content": "//\n//  ApiRepository+Post.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-03.\n//\n\nimport Foundation\n\nextension ApiRepository {\n    // swiftlint:disable:next function_parameter_count\n    func getPosts(\n        communityId: Int,\n        sort: PostSortType,\n        page: Int,\n        cursor: String?,\n        limit: Int,\n        filter: GetContentFilter? = nil,\n        showHidden: Bool = false\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?) {\n        try await performingForConnection { connection in\n            try await connection.getPosts(\n                communityId: communityId,\n                sort: sort,\n                page: page,\n                cursor: cursor,\n                limit: limit,\n                filter: filter,\n                showHidden: showHidden\n            )\n        }\n    }\n\n    // swiftlint:disable:next function_parameter_count\n    func getPosts(\n        feed: ListingType,\n        sort: PostSortType,\n        page: Int,\n        cursor: String?,\n        limit: Int,\n        filter: GetContentFilter? = nil,\n        showHidden: Bool = false\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?) {\n        try await performingForConnection { connection in\n            try await connection.getPosts(\n                feed: feed,\n                sort: sort,\n                page: page,\n                cursor: cursor,\n                limit: limit,\n                filter: filter,\n                showHidden: showHidden\n            )\n        }\n    }\n    \n    func getPosts(\n        personId: Int,\n        communityId: Int? = nil,\n        sort: PostSortType = .new,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool = false\n    ) async throws -> (person: Person3Snapshot, posts: [Post2Snapshot]) {\n        try await performingForConnection { connection in\n            try await connection.getPosts(\n                personId: personId,\n                communityId: communityId,\n                sort: sort,\n                page: page,\n                limit: limit,\n                savedOnly: savedOnly\n            )\n        }\n    }\n        \n    func getPostHistory(\n        type: GetContentFilter,\n        page: Int?,\n        cursor: String?,\n        limit: Int\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?) {\n        try await performingForConnection { connection in\n            try await connection.getPostHistory(\n                type: type,\n                page: page,\n                cursor: cursor,\n                limit: limit\n            )\n        }\n    }\n    \n    func getPost(id: Int) async throws -> Post3Snapshot {\n        try await performingForConnection { connection in\n            try await connection.getPost(id: id)\n        }\n    }\n    \n    func getPost(url: URL) async throws -> Post2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.getPost(url: url)\n        }\n    }\n    \n    // This method should be removed in favor of the below method once we drop support for versions before Lemmy 1.0\n    func searchPosts(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: PostSortType\n    ) async throws -> [Post2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.searchPosts(\n                query: query,\n                page: page,\n                limit: limit,\n                communityId: communityId,\n                creatorId: creatorId,\n                filter: filter,\n                sort: sort\n            )\n        }\n    }\n    \n    func searchPosts(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: SearchSortType\n    ) async throws -> [Post2Snapshot] {\n        try await performingForConnection { connection in\n            try await connection.searchPosts(\n                query: query,\n                page: page,\n                limit: limit,\n                communityId: communityId,\n                creatorId: creatorId,\n                filter: filter,\n                sort: sort\n            )\n        }\n    }\n    \n    func markPostAsRead(id: Int, read: Bool = true) async throws {\n        try await performingForConnection { connection in\n            try await connection.markPostAsRead(id: id, read: read)\n        }\n    }\n    \n    func markPostsAsRead(ids: Set<Int>, read: Bool = true) async throws {\n        try await performingForConnection { connection in\n            try await connection.markPostsAsRead(ids: ids, read: read)\n        }\n    }\n    \n    func voteOnPost(id: Int, score: ScoringOperation) async throws -> Post2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.voteOnPost(id: id, score: score)\n        }\n    }\n\n    func savePost(id: Int, save: Bool) async throws -> Post2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.savePost(id: id, save: save)\n        }\n    }\n    \n    func deletePost(id: Int, delete: Bool) async throws -> Post2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.deletePost(id: id, delete: delete)\n        }\n    }\n    \n    /// Added in 0.19.4\n    func hidePost(\n        id: Int,\n        hide: Bool\n    ) async throws {\n        try await performingForConnection { connection in\n            try await connection.hidePost(id: id, hide: hide)\n        }\n    }\n    \n    func createPost(\n        communityId: Int,\n        title: String,\n        content: String? = nil,\n        linkUrl: URL? = nil,\n        altText: String? = nil,\n        thumbnail: URL? = nil,\n        nsfw: Bool,\n        languageId: Int? = nil\n    ) async throws -> Post2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.createPost(\n                communityId: communityId,\n                title: title,\n                content: content,\n                linkUrl: linkUrl,\n                altText: altText,\n                thumbnail: thumbnail,\n                nsfw: nsfw,\n                languageId: languageId\n            )\n        }\n    }\n    \n    func editPost(\n        id: Int,\n        title: String,\n        content: String? = nil,\n        linkUrl: URL? = nil,\n        altText: String? = nil,\n        thumbnail: URL? = nil,\n        nsfw: Bool,\n        languageId: Int? = nil\n    ) async throws -> Post2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.editPost(\n                id: id,\n                title: title,\n                content: content,\n                linkUrl: linkUrl,\n                altText: altText,\n                thumbnail: thumbnail,\n                nsfw: nsfw,\n                languageId: languageId\n            )\n        }\n    }\n\n    func replyToPost(id: Int, content: String, languageId: Int? = nil) async throws -> Comment2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.replyToPost(id: id, content: content, languageId: languageId)\n        }\n    }\n    \n    func reportPost(id: Int, reason: String) async throws -> ReportSnapshot {\n        try await performingForConnection { connection in\n            try await connection.reportPost(id: id, reason: reason)\n        }\n    }\n    \n    func purgePost(id: Int, reason: String?) async throws {\n        try await performingForConnection { connection in\n            try await connection.purgePost(id: id, reason: reason)\n        }\n    }\n    \n    func removePost(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Post2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.removePost(id: id, remove: remove, reason: reason)\n        }\n    }\n    \n    func pinPost(\n        id: Int,\n        pin: Bool,\n        to target: PostFeatureType\n    ) async throws -> Post2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.pinPost(id: id, pin: pin, to: target)\n        }\n    }\n    \n    func lockPost(\n        id: Int,\n        lock: Bool\n    ) async throws -> Post2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.lockPost(id: id, lock: lock)\n        }\n    }\n    \n    func setPostNsfw(id: Int, nsfw: Bool) async throws -> Post1Snapshot {\n        try await performingForConnection { connection in\n            try await connection.setPostNsfw(id: id, nsfw: nsfw)\n        }\n    }\n    \n    func getPostVotes(\n        id: Int,\n        page: Int = 1,\n        limit: Int = 20\n    ) async throws -> [PersonVoteSnapshot] {\n        try await performingForConnection { connection in\n            try await connection.getPostVotes(\n                id: id,\n                page: page,\n                limit: limit\n            )\n        }\n    }\n\n    @discardableResult\n    func voteInPoll(postId: Int, choiceIds: Set<Int>) async throws -> Post2Snapshot {\n        try await performingForConnection { connection in\n            try await connection.voteInPoll(postId: postId, choiceIds: choiceIds)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+RegistrationApplication.swift",
    "content": "//\n//  ApiRepository+RegistrationApplication.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-03.\n//\n\nextension ApiRepository {\n    func getRegistrationApplicationCount() async throws -> Int {\n        try await performingForConnection { connection in\n            try await connection.getRegistrationApplicationCount()\n        }\n    }\n    \n    func getRegistrationApplications(\n        page: Int = 1,\n        limit: Int = 20,\n        unreadOnly: Bool = false\n    ) async throws -> [RegistrationApplicationSnapshot] {\n        try await performingForConnection { connection in\n            try await connection.getRegistrationApplications(\n                page: page,\n                limit: limit,\n                unreadOnly: unreadOnly\n            )\n        }\n    }\n    \n    func approveRegistrationApplication(id: Int) async throws -> RegistrationApplicationSnapshot {\n        try await performingForConnection { connection in\n            try await connection.approveRegistrationApplication(id: id)\n        }\n    }\n    \n    func denyRegistrationApplication(\n        id: Int,\n        reason: String?\n    ) async throws -> RegistrationApplicationSnapshot {\n        try await performingForConnection { connection in\n            try await connection.denyRegistrationApplication(id: id, reason: reason)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository+Report.swift",
    "content": "//\n//  ApiRepository+Report.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-03.\n//\n\nextension ApiRepository {\n    func getReportCount(communityId: Int? = nil) async throws -> ReportUnreadCountSnapshot {\n        try await performingForConnection { connection in\n            try await connection.getReportCount(communityId: communityId)\n        }\n    }\n    \n    func getPostReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false,\n        communityId: Int? = nil,\n        postId: Int? = nil\n    ) async throws -> [ReportSnapshot] {\n        try await performingForConnection { connection in\n            try await connection.getPostReports(\n                page: page,\n                limit: limit,\n                unresolvedOnly: unresolvedOnly,\n                communityId: communityId,\n                postId: postId\n            )\n        }\n    }\n    \n    func getCommentReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false,\n        communityId: Int? = nil,\n        commentId: Int? = nil\n    ) async throws -> [ReportSnapshot] {\n        try await performingForConnection { connection in\n            try await connection.getCommentReports(\n                page: page,\n                limit: limit,\n                unresolvedOnly: unresolvedOnly,\n                communityId: communityId,\n                commentId: commentId\n            )\n        }\n    }\n    \n    func getMessageReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false\n    ) async throws -> [ReportSnapshot] {\n        try await performingForConnection { connection in\n            try await connection.getMessageReports(\n                page: page,\n                limit: limit,\n                unresolvedOnly: unresolvedOnly\n            )\n        }\n    }\n    \n    func resolvePostReport(\n        id: Int,\n        resolved: Bool\n    ) async throws -> ReportSnapshot {\n        try await performingForConnection { connection in\n            try await connection.resolvePostReport(id: id, resolved: resolved)\n        }\n    }\n    \n    func resolveCommentReport(\n        id: Int,\n        resolved: Bool\n    ) async throws -> ReportSnapshot {\n        try await performingForConnection { connection in\n            try await connection.resolveCommentReport(id: id, resolved: resolved)\n        }\n    }\n    \n    func resolveMessageReport(\n        id: Int,\n        resolved: Bool\n    ) async throws -> ReportSnapshot {\n        try await performingForConnection { connection in\n            try await connection.resolveMessageReport(id: id, resolved: resolved)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ApiRepository.swift",
    "content": "//\n//  ApiRepository.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-02.\n//\n\nimport Foundation\nimport Rest\n\n/// This class represents an abstract interface on top of the underlying Connection; it is responsible for managing the Connection and\n/// serving consistent Snapshot models to higher layers.\n///\n/// The data access methods in here are all intentionally dumb--they just make calls and return snapshots. Validation, enrichment,\n/// and other business logic that must occur before serving the final model to the app should be performed by the consumer of the repository.\nclass ApiRepository {\n    private static let supportedConnections: [any InstanceConnection.Type] = [LemmyConnection.self, PieFedConnection.self]\n    \n    private struct ConnectionWrapper {\n        let wrappedValue: any InstanceConnection\n    }\n    \n    let baseUrl: URL\n    let username: String?\n    private var connectionMultiplexer: ConnectionMultiplexer<ConnectionWrapper>!\n    \n    let restClient = RestClient(errorType: ApiErrorResponse.self)\n    var token: String?\n    \n    var connection: (any InstanceConnection)? {\n        get {\n            connectionMultiplexer.selectedCandidate?.wrappedValue\n        }\n        set {\n            connectionMultiplexer.selectedCandidate = .init(wrappedValue: newValue!)\n        }\n    }\n\n    init(baseUrl: URL, username: String? = nil) {\n        self.baseUrl = baseUrl\n        self.username = username\n        \n        self.connectionMultiplexer = .init {\n            Self.supportedConnections.map { .init(wrappedValue: $0.init(baseUrl: self.baseUrl, token: self.token)) }\n        }\n    }\n    \n    func updateToken(_ newToken: String) {\n        guard username != nil else {\n            assertionFailure()\n            return\n        }\n        \n        connection?.updateToken(newToken)\n        token = newToken\n    }\n    \n    func perform<Request: RestRequest>(\n        _ request: Request,\n        tokenOverride: String? = nil,\n        requiresToken: Bool = true // This should be `true` for the vast majority of requests, even GET requests\n    ) async throws -> Request.Response {\n        guard !requiresToken || username == nil || token != nil else {\n            throw ApiClientError.noToken\n        }\n        \n        let token = tokenOverride ?? token\n        do throws(RestError) {\n            return try await restClient.perform(baseUrl: baseUrl, request, token: token)\n        } catch {\n            switch error {\n            case let RestError.response(response, statusCode: _):\n                if ApiErrorResponse(error: response).isNotLoggedIn {\n                    throw token == nil ? ApiClientError.notLoggedIn : ApiClientError.invalidSession // (self)\n                } else {\n                    throw ApiClientError(from: error)\n                }\n            default:\n                throw ApiClientError(from: error)\n            }\n        }\n    }\n    \n    func getConnection() async throws -> any InstanceConnection {\n        try await connectionMultiplexer.getConnection {\n            _ = try await getMyInstance()\n        }.wrappedValue\n    }\n    \n    @MainActor\n    func performingForConnection<T>(\n        _ callback: @escaping (any InstanceConnection) async throws -> T,\n        file: String = #fileID,\n        function: String = #function,\n        line: Int = #line\n    ) async throws -> T {\n        do {\n            return try await connectionMultiplexer.perform { wrapper in\n                try await callback(wrapper.wrappedValue)\n            }\n        } catch ConnectionMultiplexerError.allConnectionsFailed {\n            throw ApiClientError.unableToDetermineSoftware\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/API Repository/ConnectionMultiplexer.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-08-09.\n//\n\nimport Foundation\nimport os\n\nenum ConnectionMultiplexerError: Error {\n    case allConnectionsFailed\n}\n\nclass ConnectionMultiplexer<Candidate> {\n    private let log: Logger = .mlemLogger()\n    \n    private var ongoingTask: Task<Any, Error>?\n    \n    var getCandidates: () -> [Candidate]\n    var selectedCandidate: Candidate?\n    \n    init(getCandidates: @escaping () -> [Candidate]) {\n        self.getCandidates = getCandidates\n    }\n    \n    @MainActor\n    func perform<T>(\n        _ callback: @escaping (Candidate) async throws -> T\n    ) async throws -> T {\n        // Iterate through all possible candidates, and call the callback on each in turn.\n        // As soon as one of the calls succeeds, return the result and cancel the other ongoing calls.\n        // Cache the `Candidate` that succeeded in the `self.selectedCandidate` property, and use that\n        // for all subsequent calls of `perform`.\n        \n        // If `perform` is called and `self.selectedCandidate` is `nil` but there is another\n        // `perform` call ongoing, it will wait for the other call to succeed first.\n\n        _ = await self.ongoingTask?.result\n        if let selectedCandidate {\n            return try await callback(selectedCandidate)\n        }\n\n        let ongoingTask: Task<T, Error> = Task {\n            try await withThrowingTaskGroup(of: (Int, Result<T, Error>).self) { group in\n\n                let candidates = self.getCandidates()\n\n                for (index, candidate) in candidates.enumerated() {\n                    group.addTask {\n                        do {\n                            let response = try await callback(candidate)\n                            return (index, .success(response))\n                        } catch {\n                            return (index, .failure(error))\n                        }\n                    }\n                }\n                \n                var results: [(Int, Result<T, Error>)] = []\n                while !group.isEmpty {\n                    guard let result = try? await group.next() else {\n                        assertionFailure()\n                        continue\n                    }\n                    results.append(result)\n                }\n\n                results.sort(by: { $0.0 < $1.0 })\n                \n                // Find first successful result in candidate order\n                for (candidate, result) in zip(candidates, results.map(\\.1)) {\n                    do {\n                        let value = try result.get()\n                        log.info(\"Selected \\(String(describing: candidate))\")\n                        self.selectedCandidate = candidate\n                        self.ongoingTask = nil\n                        return value\n                    } catch ApiClientError.serverError(404), ApiClientError.featureUnsupported {\n                        // no-op\n                    } catch {\n                        throw error\n                    }\n                }\n                \n                throw ConnectionMultiplexerError.allConnectionsFailed\n            }\n        }\n        \n        self.ongoingTask = Task {\n            _ = try? await ongoingTask.result.get()\n        }\n        \n        return try await ongoingTask.result.get()\n    }\n    \n    func getConnection(callback: () async throws -> Void) async throws -> Candidate {\n        _ = await ongoingTask?.result\n        if let selectedCandidate {\n            return selectedCandidate\n        }\n        try await callback()\n        if let selectedCandidate {\n            return selectedCandidate\n        }\n        assertionFailure()\n        throw ApiClientError.unsuccessful\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Bridges/LemmyBlockBridge.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-11-13.\n//  \n\nimport Foundation\n\npublic struct LemmyCommunityBlockBridge: Codable, Hashable, Sendable {\n    public let community: LemmyCommunity\n    \n    public init(from decoder: any Decoder) throws {\n        if let community = try? LemmyCommunity(from: decoder) {\n            self.community = community\n            return\n        }\n        let view = try LemmyCommunityBlockView(from: decoder)\n        self.community = view.community\n    }\n}\n\npublic struct LemmyPersonBlockBridge: Codable, Hashable, Sendable {\n    public let person: LemmyPerson\n    \n    public init(from decoder: any Decoder) throws {\n        if let person = try? LemmyPerson(from: decoder) {\n            self.person = person\n            return\n        }\n        let view = try LemmyPersonBlockView(from: decoder)\n        self.person = view.target\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Bridges/LemmyInstanceWithFederationStateBridge.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-21.\n//\n\nimport Foundation\n\npublic struct LemmyInstanceWithFederationStateBridge: Codable, Hashable, Sendable {\n    let domain: String\n    \n    public init(from decoder: any Decoder) throws {\n        if let old = try? LemmyInstance(from: decoder) {\n            self.domain = old.domain\n            return\n        }\n        \n        if let new = try? LemmyInstanceWithFederationState(from: decoder) {\n            self.domain = new.domain\n            return\n        }\n        \n        throw DecodingError.dataCorrupted(\n            .init(codingPath: decoder.codingPath, debugDescription: \"LemmyInstanceWithFederationStateBridge error\")\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Bridges/LemmySortTypeBridge.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-22.\n//\n\nimport Foundation\nimport Rest\nimport URLEncoder\n\n// The `LemmySearch.sort` property uses `LemmySortType` pre-0.20 and\n// uses `LemmySearchSortType` post-0.20, even when interacting using the v3 api.\n// The type of that property is manually overriden with this type, which\n// can then be converted into either of those two types.\n\npublic typealias ApiBridgeable = Codable & Hashable & Sendable\n\npublic enum ApiBridge<OldType: ApiBridgeable, NewType: ApiBridgeable>: Codable, Hashable, Sendable {\n    case old(OldType)\n    case new(NewType)\n    \n    public typealias RawValue = String\n    \n    var value: any ApiBridgeable {\n        switch self {\n        case let .old(old): old\n        case let .new(new): new\n        }\n    }\n    \n    public static func oldOrUnsupported(_ value: OldType?) throws(ApiClientError) -> Self {\n        if let value {\n            return .old(value)\n        } else {\n            throw .featureUnsupported\n        }\n    }\n\n    public static func newOrUnsupported(_ value: NewType?) throws(ApiClientError) -> Self {\n        if let value {\n            return .new(value)\n        } else {\n            throw .featureUnsupported\n        }\n    }\n}\n\npublic extension ApiBridge {\n    init(from decoder: any Decoder) throws {\n        let container = try decoder.singleValueContainer()\n        if let new = try? container.decode(NewType.self) {\n            self = .new(new)\n            return\n        }\n        if let old = try? container.decode(OldType.self) {\n            self = .old(old)\n            return\n        }\n        throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: \"Unsupported value type\"))\n    }\n    \n    func encode(to encoder: any Encoder) throws {\n        try value.encode(to: encoder)\n    }\n}\n\npublic typealias LemmySearchSortTypeBridge = ApiBridge<LemmySortType, LemmySearchSortType>\npublic typealias LemmyPostSortTypeBridge = ApiBridge<LemmySortType, LemmyPostSortType>\npublic typealias LemmyCommunitySortTypeBridge = ApiBridge<LemmySortType, LemmyCommunitySortType>\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Bridges/LemmyVoteShowBridge.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-11-13.\n//  \n\nimport Foundation\n\npublic struct LemmyVoteShowBridge: Codable, Hashable, Sendable {\n    let voteShow: LemmyVoteShow\n    \n    public var boolValue: Bool {\n        get throws {\n            switch voteShow {\n            case .show: true\n            case .hide: false\n            case .showForOthers: throw LemmyEncodingError.lemmyVoteShowBridge\n            }\n        }\n    }\n    \n    public init(showVotes: Bool) {\n        self.voteShow = showVotes ? .show : .hide\n    }\n    \n    public init(from decoder: any Decoder) throws {\n        if let vote = try? LemmyVoteShow(from: decoder) {\n            voteShow = vote\n        } else {\n            let bool = try Bool(from: decoder)\n            self.voteShow = bool ? .show : .hide\n        }\n    }\n    \n    public func encode(to encoder: any Encoder) throws {\n        switch try encoder.endpointVersion {\n        case .v3: try boolValue.encode(to: encoder)\n        case .v4: try voteShow.encode(to: encoder)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Constants/MiddlewareConstants.swift",
    "content": "//\n//  App Constants.swift\n//  Mlem\n//\n//  Created by David Bureš on 03.05.2023.\n//\n\nimport Foundation\n\nenum MiddlewareConstants {\n    static let infiniteLoadThresholdOffset: Int = 10\n    static let maxRetries: Int = 3\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ActiveUserCount.swift",
    "content": "//\n//  ActiveUserCount.swift\n//\n//\n//  Created by Sjmarf on 29/05/2024.\n//\n\nimport Foundation\n\npublic struct ActiveUserCount: Equatable {\n    public let sixMonths: Int\n    public let month: Int\n    public let week: Int\n    public let day: Int\n    \n    public init(sixMonths: Int, month: Int, week: Int, day: Int) {\n        self.sixMonths = sixMonths\n        self.month = month\n        self.week = week\n        self.day = day\n    }\n    \n    public static let zero: ActiveUserCount = .init(sixMonths: 0, month: 0, week: 0, day: 0)\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ActorIdentifiable.swift",
    "content": "//\n//  ActorIdentifiable.swift\n//  Mlem\n//\n//  Created by Sjmarf on 15/02/2024.\n//\n\nimport Foundation\n\n/// Represents a Lemmy entity that can be represented by an ``ActorIdentifier``.\npublic protocol ActorIdentifiable {\n    // An identifier that is unique across Lemmy instances.\n    var actorId: ActorIdentifier { get }\n}\n\npublic extension ActorIdentifiable {\n    @inlinable\n    var host: String { actorId.host }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ActorIdentifier.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-01-25.\n//\n\nimport Foundation\n\n/// An identifier for an ActivityPub entity that is unique across all federated instances. This is a wrapper of `URL`.\n///\n/// ## Discussion\n///\n/// Avoid instantiating`ActorIdentifier`directly, and instead obtain\n/// instances by interacting with `ApiClient`.\n///\n/// Lemmy uses the following ActorIdentifier formats (this list may be incomplete, I'm not sure):\n/// - `https://example.com`\n/// - `https://example.com/c/name`\n/// - `https://example.com/u/name`\n/// - `https://example.com/post/123`\n/// - `https://example.com/comment/123`\n/// - `https://example.com/private_message/123`\n///\n/// In addition to these formats, an ActorIdentifier may use a non-Lemmy format such as:\n/// - `https://fedia.io/m/fedia` (Community URL for Kbin/Mbin)\n/// - `https://misskey.io/users/9h75uqwaa8` (Person URL for Misskey)\n///\n/// It should be noted that private messages cannot be resolved using ``ResolveObjectRequest``.\n///\npublic struct ActorIdentifier: Hashable, Sendable {\n    public let url: URL\n    public let host: String\n    \n    /// Create an `ActorIdentifier` from a given URL.\n    ///\n    /// When you use this method, you *must* be sure that the provided URL is the actual ActivityPub\n    /// ID for the given entity, and not just any URL pointing to it. If possible, avoid using this initialiser.\n    ///\n    public init?(url: URL) {\n        guard let host = url.host() else { return nil }\n        self.init(url: url, host: host)\n    }\n    \n    private init(url: URL, host: String) {\n        if url.pathComponents.isEmpty {\n            self.url = url.appendingPathComponent(\"/\")\n        } else {\n            self.url = url\n        }\n        self.host = host\n    }\n    \n    public static func instance(host: String) -> Self {\n        var components = URLComponents()\n        components.scheme = \"https\"\n        components.host = host\n        return ActorIdentifier(url: components.url!, host: host)\n    }\n\n    public var hostUrl: URL {\n        var components = URLComponents()\n        components.scheme = \"https\"\n        components.host = host\n        return components.url! // This will always succeed\n    }\n}\n\nextension ActorIdentifier: Codable {\n    public init(from decoder: any Decoder) throws {\n        let container = try decoder.singleValueContainer()\n        let string = try container.decode(String.self)\n        guard let url = URL(string: string) else { throw Self.DecodingError.invalidUrl }\n        if let actorId = ActorIdentifier(url: url) {\n            self = actorId\n        } else {\n            throw Self.DecodingError.invalidUrl\n        }\n    }\n    \n    public func encode(to encoder: any Encoder) throws {\n        var container = encoder.singleValueContainer()\n        try container.encode(url)\n    }\n}\n\nextension ActorIdentifier: CustomStringConvertible {\n    public var description: String { url.description }\n}\n\nextension ActorIdentifier: CustomDebugStringConvertible {\n    public var debugDescription: String { \"ActorIdentifier(\\(url.description))\" }\n}\n\npublic extension ActorIdentifier {\n    enum EntityType {\n        case post, comment, message, person, community, instance\n    }\n    \n    enum DecodingError: Error {\n        case invalidUrl\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/BlockList.swift",
    "content": "//\n//  BlockList.swift\n//\n//\n//  Created by Sjmarf on 08/07/2024.\n//\n\nimport Foundation\n\n@Observable\npublic class BlockList {\n    private let api: ApiClient\n\n    /// Mapping `actorId` to `id`.\n    var people: [ActorIdentifier: Int] = .init()\n    /// Mapping `actorId` to `id`.\n    var communities: [ActorIdentifier: Int] = .init()\n    /// Mapping `actorId` to `instanceId`.\n    var instances: [ActorIdentifier: Int] = .init()\n\n    init(\n        api: ApiClient,\n        people: [ActorIdentifier: Int],\n        communities: [ActorIdentifier: Int],\n        instances: [ActorIdentifier: Int]\n    ) {\n        self.api = api\n        self.people = people\n        self.communities = communities\n        self.instances = instances\n    }\n    \n    convenience init(api: ApiClient, blocks: BlockListSnapshot) {\n        self.init(\n            api: api,\n            people: blocks.people,\n            communities: blocks.communities,\n            instances: blocks.instances\n        )\n    }\n    \n    func update(blocks: BlockListSnapshot) {\n        // People\n        \n        let oldPeopleKeys = Set(people.keys)\n        let newPeopleKeys = Set(blocks.people.keys)\n\n        // bypasses queuing for blocked status\n        for key in newPeopleKeys.subtracting(oldPeopleKeys) {\n            if let id = blocks.people[key], let person = api.caches.person.retrieveModel(cacheId: id) {\n                person.blocked_.set(true)\n            }\n        }\n        for key in oldPeopleKeys.subtracting(newPeopleKeys) {\n            if let id = people[key], let person = api.caches.person.retrieveModel(cacheId: id) {\n                person.blocked_.set(false)\n            }\n        }\n        \n        // Communities\n        \n        let oldCommunitiesKeys = Set(communities.keys)\n        let newCommunitiesKeys = Set(blocks.communities.keys)\n\n        // bypasses queuing for blocked status\n        for key in newCommunitiesKeys.subtracting(oldCommunitiesKeys) {\n            if let id = blocks.communities[key], let community = api.caches.community.retrieveModel(cacheId: id) {\n                community.blocked_.set(true)\n            }\n        }\n        for key in oldCommunitiesKeys.subtracting(newCommunitiesKeys) {\n            if let id = communities[key], let community = api.caches.community.retrieveModel(cacheId: id) {\n                community.blocked_.set(false)\n            }\n        }\n        \n        // Instances\n        \n        let oldInstancesKeys = Set(instances.keys)\n        let newInstancesKeys = Set(blocks.instances.keys)\n\n        for key in newInstancesKeys.subtracting(oldInstancesKeys) {\n            if let id = blocks.instances[key], let instance = api.caches.instance.retrieveModel(instanceId: id) {\n                instance.blocked_.set(true)\n            }\n        }\n        for key in oldInstancesKeys.subtracting(newInstancesKeys) {\n            if let id = instances[key], let instance = api.caches.instance.retrieveModel(instanceId: id) {\n                instance.blocked_.set(false)\n            }\n        }\n\n        people = blocks.people\n        communities = blocks.communities\n        instances = blocks.instances\n    }\n    \n    public func contains(personActorId: ActorIdentifier) -> Bool {\n        people.keys.contains(personActorId)\n    }\n    \n    public func contains(_ person: Person) -> Bool {\n        people.keys.contains(person.actorId)\n    }\n    \n    public func contains(communityActorId: ActorIdentifier) -> Bool {\n        communities.keys.contains(communityActorId)\n    }\n    \n    public func contains(_ community: Community) -> Bool {\n        communities.keys.contains(community.actorId)\n    }\n    \n    public func contains(instanceActorId: ActorIdentifier) -> Bool {\n        instances.keys.contains(instanceActorId)\n    }\n    \n    public func contains(_ instance: Instance) -> Bool {\n        instances.keys.contains(instance.actorId)\n    }\n    \n    public func idOfBlockedPerson(actorId: ActorIdentifier) -> Int? { people[actorId] }\n    public func idOfBlockedCommunity(actorId: ActorIdentifier) -> Int? { communities[actorId] }\n    public func instanceIdOfBlockedInstance(actorId: ActorIdentifier) -> Int? { instances[actorId] }\n    \n    public var personCount: Int { people.count }\n    public var communityCount: Int { communities.count }\n    public var instanceCount: Int { instances.count }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/BlockListSnapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-06.\n//\n\nimport Foundation\n\npublic struct BlockListSnapshot {\n    /// Mapping `actorId` to `id`.\n    var people: [ActorIdentifier: Int] = .init()\n    /// Mapping `actorId` to `id`.\n    var communities: [ActorIdentifier: Int] = .init()\n    /// Mapping `actorId` to `instanceId`.\n    var instances: [ActorIdentifier: Int] = .init()\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/CanModerateProviding.swift",
    "content": "//\n//  CanModerateProviding.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-15.\n//\n\nimport Foundation\n\npublic protocol CanModerateProviding: ContentIdentifiable {\n    var canModerate: Bool { get }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Captcha.swift",
    "content": "//\n//  Captcha.swift\n//\n//\n//  Created by Sjmarf on 06/09/2024.\n//\n\nimport Foundation\n\npublic struct Captcha: Identifiable {\n    public let id: UUID\n    public let imageData: Data\n    \n    init(id: UUID, imageData: Data) {\n        self.id = id\n        self.imageData = imageData\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/CaptchaDifficulty.swift",
    "content": "//\n//  CaptchaDifficulty.swift\n//\n//\n//  Created by Sjmarf on 28/05/2024.\n//\n\nimport Foundation\n\npublic enum CaptchaDifficulty: String, Codable {\n    case easy, medium, hard\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Comment/Comment+Conformance.swift",
    "content": "//\n//  Comment+Conformance.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-19.\n//\n\nimport Foundation\n\n// MARK: CacheIdentifiable\n\npublic extension Comment {\n    var cacheId: Int { id }\n}\n\n// MARK: FeedLoadable\n\npublic extension Comment {\n    typealias FilterType = CommentFilterType\n    \n    func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        switch sortType {\n        case .new:\n            return .new(created)\n        }\n    }\n}\n\n// MARK: SelectableContentProviding\n\npublic extension Comment {\n    var selectableContent: String? { content }\n}\n\n// MARK: ContentIdentifiable\n\npublic extension Comment {\n    static var modelTypeId: ContentType { .comment }\n}\n\n// MARK: OwnershipProviding\n\npublic extension Comment {\n    func isOwnContent(myPersonId: Int) -> Bool {\n        creatorId == myPersonId\n    }\n}\n\n// MARK: Resolvable\n\npublic extension Comment {\n    /// Returns a `URL` that can be resolved by another `ApiClient`.\n    func resolvableUrl(from instance: ContentModelUrlType) -> URL {\n        switch instance {\n        case .host: actorId.url\n        case .provider: .comment(host: api.host, id: id)\n        }\n    }\n    \n    @inlinable\n    var allResolvableUrls: [URL] {\n        ContentModelUrlType.allCases.map { resolvableUrl(from: $0) }\n    }\n}\n\n// MARK: Sharable\n\npublic extension Comment {\n    func url() -> URL { api.baseUrl.appending(path: \"comment/\\(id)\") }\n}\n\n// MARK: InteractableProviding\n\npublic extension Comment {\n    var downvotesEnabled: Bool {\n        api.voteFederationMode.commentDownvote != .disable\n    }\n}\n\n// MARK: CanModerateProviding\n\npublic extension Comment {\n    var canModerate: Bool {\n        guard let id = community.value_?.id as? Int,\n              let myPersonModerates = api.myPerson?.moderates else { return false }\n        return myPersonModerates(.id(id)) || api.isAdmin\n    }\n}\n\n// MARK: CommentResolvable\n\npublic extension Comment {\n    func asComment() async throws -> Comment { self }\n}\n\n// MARK: PersonContentProviding\n\npublic extension Comment {\n    var userContent: PersonContent { .init(wrappedValue: .comment(self)) }\n}\n\n// MARK: ReportableProviding\n\npublic extension Comment {\n    func report(reason: String) async throws {\n        try await api.reportComment(id: id, reason: reason)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Comment/Comment+Mock.swift",
    "content": "//\n//  Comment1+Mock.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-03-15.\n//\n\nimport Foundation\n\n// TODO: updated mocks\n//#if DEBUG\n//    public extension Comment1 {\n//        static func mock(\n//            api: MockApiClient = .mock,\n//            actorId: ActorIdentifier? = nil,\n//            id: Int,\n//            content: String,\n//            removed: Bool,\n//            created: Date,\n//            updated: Date?,\n//            deleted: Bool,\n//            creatorId: Int,\n//            postId: Int,\n//            parentCommentIds: [Int],\n//            distinguished: Bool,\n//            languageId: Int\n//        ) -> Comment1 {\n//            .init(\n//                api: api,\n//                actorId: actorId ?? .init(url: URL(string: \"https://\\(api.host)/comment/\\(id)\")!)!,\n//                id: id,\n//                content: content,\n//                removed: removed,\n//                created: created,\n//                updated: updated,\n//                deleted: deleted,\n//                creatorId: creatorId,\n//                postId: postId,\n//                parentCommentIds: parentCommentIds,\n//                distinguished: distinguished,\n//                languageId: languageId\n//            )\n//        }\n//    }\n//#endif\n\n//#if DEBUG\n//    public extension Comment2 {\n//        static func mock(\n//            api: ApiClient,\n//            comment1: Comment1,\n//            creator: Person1,\n//            post: UnifiedPostModel,\n//            community: Community1,\n//            votes: VotesModel,\n//            saved: Bool,\n//            creatorIsModerator: Bool,\n//            creatorIsAdmin: Bool,\n//            bannedFromCommunity: Bool,\n//            commentCount: Int\n//        ) -> Comment2 {\n//            assert(api == comment1.api)\n//            assert(api == creator.api)\n//            assert(api == community.api)\n//            assert(api == post.api)\n//            return .init(\n//                api: api,\n//                comment1: comment1,\n//                creator: creator,\n//                post: post,\n//                community: community,\n//                votes: votes,\n//                saved: saved,\n//                creatorIsModerator: creatorIsModerator,\n//                creatorIsAdmin: creatorIsAdmin,\n//                creatorBannedFromCommunity: bannedFromCommunity,\n//                commentCount: commentCount\n//            )\n//        }\n//    }\n//#endif\n\n//extension Comment2 {\n//    var apiCommentView: LemmyCommentView {\n//        LemmyCommentView(\n//            comment: comment1.apiComment,\n//            creator: creator.apiPerson,\n//            post: post.apiPost,\n//            community: community.apiCommunity,\n//            counts: .init(\n//                commentId: id,\n//                score: votes.total,\n//                upvotes: votes.upvotes,\n//                downvotes: votes.downvotes,\n//                published: created,\n//                childCount: commentCount\n//            ),\n//            creatorBannedFromCommunity: creator.isBannedFromCommunity(id: community.id) ?? false,\n//            creatorIsModerator: creatorIsModerator,\n//            creatorIsAdmin: creatorIsAdmin,\n//            subscribed: .notSubscribed,\n//            saved: saved,\n//            creatorBlocked: creator.blocked,\n//            myVote: votes.myVote.rawValue,\n//            bannedFromCommunity: false,\n//            communityActions: nil,\n//            commentActions: nil,\n//            personActions: nil,\n//            postTags: nil,\n//            canMod: nil,\n//            creatorBanned: nil,\n//            creatorBanExpiresAt: nil,\n//            creatorCommunityBanExpiresAt: nil\n//        )\n//    }\n//}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Comment/Comment.swift",
    "content": "//\n//  Comment.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-19.\n//\n\nimport Observation\nimport Foundation\n\n@Observable\npublic class Comment:\n    UnifiedModelProviding,\n    FeedLoadable,\n    SelectableContentProviding,\n    ContentIdentifiable,\n    OwnershipProviding,\n    InteractableProviding,\n    DeletableProviding,\n    PurgableProviding,\n    CommentResolvable,\n    Sharable,\n    PersonContentProviding {\n    public typealias Properties = CommentProperties\n    \n    public var api: ApiClient\n    private let properties: CommentProperties\n    @ObservationIgnored lazy var updateQueue: UnifiedUpdateQueue<Comment> = .init(parent: self, properties: properties)\n    \n    // MARK: Custom Properties\n    // Mlem-specific properties that are not reflected in the API\n    \n    public var removedPending: Bool = false\n    public var purged: Bool = false\n    \n    // MARK: API Properties\n    // Properties that are provided by the API\n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let creatorId: Int\n    public let postId: Int\n    public let parentCommentIds: [Int]\n    public let created: Date\n    public var content: String\n    public var updated: Date?\n    public var distinguished: Bool\n    public var languageId: Int\n    public var deleted: Bool\n    public var removed: Bool\n    \n    // from Comment2Snapshot\n    public var creator: ExpectedValue<Person>\n    public var post: ExpectedValue<Post>\n    public var community: ExpectedValue<Community>\n    public var commentCount: ExpectedValue<Int>\n    public var creatorIsModerator: ExpectedValue<Bool>\n    public var creatorIsAdmin: ExpectedValue<Bool>\n    public var creatorBannedFromCommunity: ExpectedValue<Bool>\n    public var votes: ExpectedValue<VotesModel>\n    public var saved: ExpectedValue<Bool>\n    \n    public init(api: ApiClient, properties: CommentProperties) {\n        self.api = api\n        self.properties = properties\n        \n        self.actorId = properties.actorId\n        self.id = properties.id\n        self.creatorId = properties.creatorId\n        self.postId = properties.postId\n        self.parentCommentIds = properties.parentCommentIds\n        self.created = properties.created\n        self.content = properties.content\n        self.updated = properties.updated\n        self.distinguished = properties.distinguished\n        self.languageId = properties.languageId\n        self.deleted = properties.deleted\n        self.removed = properties.removed\n        \n        // because upgrade() is not available until all properties are initialized, first populate all properties\n        // with ExpectedValues that don't actually do anything, then reassign them properly at the end of the init\n        // this is somewhat cumbersome but avoids lazy vars, which are very awkward in Observables\n        self.creator = dummyExpectedValue(properties.creator)\n        self.post = dummyExpectedValue(properties.post)\n        self.community = dummyExpectedValue(properties.community)\n        self.commentCount = dummyExpectedValue(properties.commentCount)\n        self.creatorIsModerator = dummyExpectedValue(properties.creatorIsModerator)\n        self.creatorIsAdmin = dummyExpectedValue(properties.creatorIsAdmin)\n        self.creatorBannedFromCommunity = dummyExpectedValue(properties.creatorBannedFromCommunity)\n        self.votes = dummyExpectedValue(properties.votes)\n        self.saved = dummyExpectedValue(properties.saved)\n        \n        func expectedValue<T>(_ value: T?) -> ExpectedValue<T> {\n            .init(\n                value: value,\n                provideValue: { try await self.upgrade() })\n        }\n        self.creator = expectedValue(properties.creator)\n        self.post = expectedValue(properties.post)\n        self.community = expectedValue(properties.community)\n        self.commentCount = expectedValue(properties.commentCount)\n        self.creatorIsModerator = expectedValue(properties.creatorIsModerator)\n        self.creatorIsAdmin = expectedValue(properties.creatorIsAdmin)\n        self.creatorBannedFromCommunity = expectedValue(properties.creatorBannedFromCommunity)\n        self.votes = expectedValue(properties.votes)\n        self.saved = expectedValue(properties.saved)\n    }\n    \n    @MainActor\n    public func update(with properties: CommentProperties) {\n        if !properties.removed {\n            setIfChanged(\\.content, properties.content)\n        }\n        setIfChanged(\\.updated, properties.updated)\n        setIfChanged(\\.distinguished, properties.distinguished)\n        setIfChanged(\\.languageId, properties.languageId)\n        setIfChanged(\\.deleted, properties.deleted)\n        setIfChanged(\\.removed, properties.removed)\n        \n        setIfNil(\\.creator.value_, properties.creator)\n        setIfNil(\\.post.value_, properties.post)\n        setIfNil(\\.community.value_, properties.community)\n        updateIfChanged(\\.commentCount.value_, properties.commentCount)\n        updateIfChanged(\\.creatorIsModerator.value_, properties.creatorIsModerator)\n        updateIfChanged(\\.creatorIsAdmin.value_, properties.creatorIsAdmin)\n        updateIfChanged(\\.creatorBannedFromCommunity.value_, properties.creatorBannedFromCommunity)\n        updateIfChanged(\\.votes.value_, properties.votes)\n        updateIfChanged(\\.saved.value_, properties.saved)\n    }\n    \n    @MainActor\n    public func softUpdate(with properties: CommentProperties) {\n        setIfNil(\\.creator.value_, properties.creator)\n        setIfNil(\\.post.value_, properties.post)\n        setIfNil(\\.community.value_, properties.community)\n        setIfNil(\\.commentCount.value_, properties.commentCount)\n        setIfNil(\\.creatorIsModerator.value_, properties.creatorIsModerator)\n        setIfNil(\\.creatorIsAdmin.value_, properties.creatorIsAdmin)\n        setIfNil(\\.creatorBannedFromCommunity.value_, properties.creatorBannedFromCommunity)\n        setIfNil(\\.votes.value_, properties.votes)\n        setIfNil(\\.saved.value_, properties.saved)\n    }\n    \n    // TODO: unified models move these into ContentModel\n    public func upgrade() async throws {\n        try await updateQueue.upgrade()\n    }\n    \n    public func refresh() async throws {\n        try await updateQueue.refresh()\n    }\n    \n    public func fetchUpgraded() async throws -> CommentProperties {\n        let snapshot = try await api.repository.getComment(id: id)\n        return await .init(api: api, snapshot: .comment2(snapshot))\n    }\n    \n    public func resolve(with api: ApiClient) async throws -> Self {\n        let stub = CommentStub(api: api, url: allResolvableUrls[0])\n        return try await stub.asComment() as! Self\n    }\n}\n\n// MARK: - Computed\n\npublic extension Comment {\n    var depth: Int { parentCommentIds.count }\n    \n    var parentCommentId: Int? { parentCommentIds.last }\n}\n\n// MARK: - Interactions\n\npublic extension Comment {\n    \n    // Vote\n    \n    var updateVote: ((ScoringOperation) -> Void)? {\n        if let votes = votes.value {\n            return { self.updateVote($0, votes: votes) }\n        }\n        return nil\n    }\n    \n    private func updateVote(_ newValue: ScoringOperation, votes: VotesModel) {\n        self.votes.value_ = votes.applyScoringOperation(operation: newValue)\n        \n        Task {\n            await updateQueue.addItem {\n                try await .init(api: self.api, snapshot: .comment2(self.api.repository.voteOnComment(id: self.id, score: newValue)))\n            }\n        }\n    }\n    \n    // Save\n    \n    func updateSaved(_ newValue: Bool) {\n        saved.value_ = newValue\n        \n        Task {\n            await updateQueue.addItem {\n                try await .init(api: self.api, snapshot: .comment2(self.api.repository.saveComment(id: self.id, save: newValue)))\n            }\n        }\n    }\n    \n    // Remove\n    \n    func updateRemoved(_ newValue: Bool, reason: String?, callback: ((UpdateStatus) -> Void)?) {\n        removed = newValue\n        removedPending = true\n        Task {\n            await updateQueue.addItem {\n                do {\n                    let snapshot = try await self.api.repository.removeComment(id: self.id, remove: newValue, reason: reason)\n                    callback?(.success)\n                    return await .init(api: self.api, snapshot: .comment2(snapshot))\n                } catch {\n                    callback?(.failure(error))\n                    throw (error)\n                }\n            }\n        }\n    }\n    \n    // Reply\n    \n    func reply(content: String, languageId: Int? = nil) async throws -> Comment {\n        try await api.replyToComment(postId: postId, parentId: id, content: content, languageId: languageId)\n    }\n    \n    // Purge\n    \n    func purge(reason: String?) async throws {\n        try await api.purgeComment(id: id, reason: reason)\n    }\n    \n    // Delete\n    \n    func updateDeleted(_ newValue: Bool, callback: ((UpdateStatus) -> Void)?) {\n        deleted = newValue\n        Task {\n            await updateQueue.addItem {\n                do {\n                    let snapshot = try await self.api.repository.deleteComment(id: self.id, delete: newValue)\n                    callback?(.success)\n                    return await .init(api: self.api, snapshot: .comment2(snapshot))\n                } catch {\n                    callback?(.failure(error))\n                    throw (error)\n                }\n            }\n        }\n    }\n    \n    // Edit\n    \n    func edit(content: String, languageId: Int?) async throws {\n        self.content = content\n        if let languageId {\n            self.languageId = languageId\n        }\n        Task {\n            await updateQueue.addItem {\n                try await .init(\n                    api: self.api,\n                    snapshot: .comment2(self.api.repository.editComment(id: self.id, content: content, languageId: languageId)))\n            }\n        }\n    }\n    \n    // Get associated models\n    \n    /// Get the parent comment, or return `nil` if there is no parent\n    func getParent(cachedValueAcceptable: Bool = false) async throws -> Comment? {\n        if let parentId = parentCommentIds.last {\n            if cachedValueAcceptable, let comment = api.caches.comment.retrieveModel(cacheId: parentId) { return comment }\n            return try await api.getComment(id: parentId)\n        }\n        return nil\n    }\n    \n    func getParents() async throws -> [Comment] {\n        guard let first = parentCommentIds.first else { return [] }\n        let comments = try await api.getComments(\n            parentId: first,\n            sort: .new,\n            page: 1,\n            maxDepth: parentCommentIds.count,\n            limit: 1000\n        )\n        var i = 0\n        return comments.filter { comment in\n            if comment.id == parentCommentIds[i] {\n                i += 1\n                return true\n            }\n            return false\n        }\n    }\n    \n    func getChildren(\n        sort: CommentSortType = .hot,\n        includedParentCount: Int = 0,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment] {\n        let parentId: Int\n        if includedParentCount <= 0 {\n            parentId = id\n        } else {\n            parentId = parentCommentIds.dropLast(includedParentCount - 1).last ?? parentCommentIds.first ?? id\n        }\n        let comments = try await api.getComments(\n            parentId: parentId,\n            sort: sort,\n            page: page,\n            maxDepth: maxDepth,\n            limit: limit,\n            filter: filter\n        )\n        if includedParentCount <= 0 {\n            return comments\n        }\n        \n        return comments.filter { $0.parentCommentIds.contains(id) || self.parentCommentIds.contains($0.id) || $0.id == self.id }\n    }\n    \n    func getVotes(page: Int, limit: Int, communityId: Int) async throws -> [PersonVote] {\n        try await api.getCommentVotes(id: id, communityId: communityId, page: page, limit: limit)\n    }\n}\n\n// MARK: Shim\n\npublic extension Comment {\n    func takeSnapshot2() -> Comment2Snapshot? {\n        guard let creator = creator.value_,\n              let post = post.value_,\n              let community = community.value_,\n              let commentCount = commentCount.value_,\n              let creatorIsModerator = creatorIsModerator.value_,\n              let creatorIsAdmin = creatorIsAdmin.value_,\n              let creatorBannedFromCommunity = creatorBannedFromCommunity.value_,\n              let votes = votes.value_,\n              let saved = saved.value_ else {\n            assertionFailure(\"takeSnapshot2() called without high-tier fields available\")\n            return nil\n        }\n        \n        return .init(comment:\n                .init(actorId: actorId,\n                      id: id,\n                      creatorId: creatorId,\n                      postId: postId,\n                      parentCommentIds: parentCommentIds,\n                      created: created,\n                      content: content,\n                      updated: updated,\n                      distinguished: distinguished,\n                      languageId: languageId,\n                      deleted: deleted,\n                      removed: removed),\n                     creator: creator.takeSnapshot1(),\n                     post: .init(\n                        actorId: post.actorId,\n                        id: post.id,\n                        creatorId: post.creatorId,\n                        communityId: post.communityId,\n                        created: post.created,\n                        title: post.title,\n                        content: post.content,\n                        linkUrl: post.linkUrl,\n                        embed: post.embed,\n                        poll: post.poll,\n                        nsfw: post.nsfw,\n                        thumbnailUrl: post.thumbnailUrl,\n                        updated: post.updated,\n                        languageId: post.languageId,\n                        altText: post.altText,\n                        deleted: post.deleted,\n                        removed: post.removed,\n                        pinnedCommunity: post.pinnedCommunity,\n                        pinnedInstance: post.pinnedInstance,\n                        locked: post.locked),\n                     community: community.takeSnapshot1(),\n                     commentCount: commentCount,\n                     creatorIsModerator: creatorIsModerator,\n                     creatorIsAdmin: creatorIsAdmin,\n                     creatorBannedFromCommunity: creatorBannedFromCommunity,\n                     votes: votes,\n                     saved: saved)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Comment/CommentProducing.swift",
    "content": "//\n//  CommentProducing.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-24.\n//\n\n/// Protocol describing things that can be resolved to a comment\npublic protocol CommentResolvable {\n    func asComment() async throws -> Comment\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Comment/CommentProperties.swift",
    "content": "//\n//  CommentProperties.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-19.\n//\n\nimport Foundation\n\npublic struct CommentProperties: UnifiedPropertiesProviding {\n    // From Comment1Snapshot, guaranteed to always be present\n    let actorId: ActorIdentifier\n    let id: Int\n    let creatorId: Int\n    let postId: Int\n    let parentCommentIds: [Int]\n    let created: Date\n    var content: String\n    var updated: Date?\n    var distinguished: Bool\n    var languageId: Int\n    var deleted: Bool\n    var removed: Bool\n    \n    // from Comment2Snapshot\n    var creator: Person?\n    var post: Post?\n    var community: Community?\n    var commentCount: Int?\n    var creatorIsModerator: Bool?\n    var creatorIsAdmin: Bool?\n    var creatorBannedFromCommunity: Bool?\n    var votes: VotesModel?\n    var saved: Bool?\n    \n    /// Constructs a CommentProperties from a given snapshot\n    @MainActor\n    public init(api: ApiClient, snapshot: AnyCommentSnapshot) {\n        let snapshot1: Comment1Snapshot\n        let snapshot2: Comment2Snapshot?\n        switch snapshot {\n        case let .comment1(comment1Snapshot):\n            snapshot1 = comment1Snapshot\n            snapshot2 = nil\n        case let .comment2(comment2Snapshot):\n            snapshot1 = comment2Snapshot.comment\n            snapshot2 = comment2Snapshot\n        }\n        \n        if let snapshot2 {\n            let newCreator: Person = api.caches.person.getModel(api: api, from: .person1(snapshot2.creator))\n            newCreator.updateKnownCommunityBanState(id: snapshot2.community.id, banned: snapshot2.creatorBannedFromCommunity)\n            \n            creator = newCreator\n            post = api.caches.post.getModel(api: api, from: .post1(snapshot2.post))\n            community = api.caches.community.getModel(api: api, from: .community1(snapshot2.community))\n            commentCount = snapshot2.commentCount\n            creatorIsModerator = snapshot2.creatorIsModerator\n            creatorIsAdmin = snapshot2.creatorIsAdmin\n            creatorBannedFromCommunity = snapshot2.creatorBannedFromCommunity\n            votes = snapshot2.votes\n            saved = snapshot2.saved\n        }\n        \n        actorId = snapshot1.actorId\n        id = snapshot1.id\n        creatorId = snapshot1.creatorId\n        postId = snapshot1.postId\n        parentCommentIds = snapshot1.parentCommentIds\n        created = snapshot1.created\n        content = snapshot1.content\n        updated = snapshot1.updated\n        distinguished = snapshot1.distinguished\n        languageId = snapshot1.languageId\n        deleted = snapshot1.deleted\n        removed = snapshot1.removed\n    }\n    \n    public mutating func merge(_ other: CommentProperties) {\n        // tier 1 properties: simple assignment\n        self.content = other.content\n        self.updated = other.updated\n        self.distinguished = other.distinguished\n        self.languageId = other.languageId\n        self.deleted = other.deleted\n        self.removed = other.removed\n        \n        // tier 2 properties: only assign if incoming non-nil\n        self.creator = other.creator ?? self.creator\n        self.post = other.post ?? self.post\n        self.community = other.community ?? self.community\n        self.commentCount = other.commentCount ?? self.commentCount\n        self.creatorIsModerator = other.creatorIsModerator ?? self.creatorIsModerator\n        self.creatorIsAdmin = other.creatorIsAdmin ?? self.creatorIsAdmin\n        self.creatorBannedFromCommunity = other.creatorBannedFromCommunity ?? self.creatorBannedFromCommunity\n        self.votes = other.votes ?? self.votes\n        self.saved = other.saved ?? self.saved\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Comment/CommentStub.swift",
    "content": "//\n//  CommentStub.swift\n//\n//\n//  Created by Sjmarf on 24/06/2024.\n//\n\nimport Foundation\n\npublic struct CommentStub: Hashable, CommentResolvable {\n    public var api: ApiClient\n    public let url: URL\n    \n    public init(api: ApiClient, url: URL) {\n        self.api = api\n        self.url = url\n    }\n    \n    public func asLocal() -> Self {\n        .init(api: .getApiClient(url: url.removingPathComponents(), username: nil), url: url)\n    }\n    \n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(url)\n    }\n    \n    public static func == (lhs: CommentStub, rhs: CommentStub) -> Bool {\n        lhs.url == rhs.url\n    }\n    \n    public func asComment() async throws -> Comment {\n        try await api.getComment(url: resolvableUrl)\n    }\n}\n\n// Resolvable conformance\npublic extension CommentStub {\n    var resolvableUrl: URL { url }\n    \n    @inlinable\n    var allResolvableUrls: [URL] { [resolvableUrl] }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Community/Community+Conformance.swift",
    "content": "//\n//  Community+Conformance.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-02-14.\n//\n\nimport Foundation\n\n// MARK: CacheIdentifiable\n\npublic extension Community {\n    var cacheId: Int { id }\n}\n\n// MARK: CommunityOrPerson\n\npublic extension Community {\n    static var identifierPrefix: String { \"!\" }\n}\n\n// MARK: ProfileProviding\n\npublic extension Community {\n    var profileCreated: Date? { created }\n}\n\n// MARK: Blockable\n\npublic extension Community {\n    var updateBlocked: ((Bool, ((Bool) -> Void)?) -> Void)? { self._updateBlocked }\n    \n    private func _updateBlocked(_ newValue: Bool, callback: ((Bool) -> Void)? = nil) {\n        let oldValue = blocked_.realizedValue\n        blocked_.set(newValue)\n        \n        Task {\n            await updateQueue.addItem {\n                do {\n                    let snapshot = try await self.api.repository.blockCommunity(id: self.id, block: newValue)\n                    callback?(true)\n                    if newValue {\n                        self.api.blocks?.communities[self.actorId] = self.id\n                    } else {\n                        self.api.blocks?.communities.removeValue(forKey: self.actorId)\n                    }\n                    return await .init(api: self.api, snapshot: .community2(snapshot))\n                } catch {\n                    // need to manually roll back because blocked is not included in snapshot informatoin\n                    self.blocked_.set(oldValue)\n                    callback?(false)\n                    throw error\n                }\n            }\n        }\n    }\n}\n\n// MARK: ContentIdentifiable\n\npublic extension Community {\n    static var modelTypeId: ContentType { .community }\n}\n\n// MARK: CanModerateProviding\n\npublic extension Community {\n    var canModerate: Bool {\n        guard let myPersonModerates = api.myPerson?.moderates else { return false }\n        return myPersonModerates(.id(id)) || api.isAdmin\n    }\n}\n\n// MARK: FeedLoadable\n\npublic extension Community {\n    typealias FilterType = CommunityFilterType\n    \n    func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        switch sortType {\n        case .new:\n            return .new(created)\n        }\n    }\n}\n\n// MARK: Sharable\n\npublic extension Community {\n    func url() -> URL {\n        if apiIsLocal {\n            api.baseUrl.appending(path: \"c/\\(name)\")\n        } else {\n            api.baseUrl.appending(path: \"c/\\(name)@\\(host)\")\n        }\n    }\n}\n\n// MARK: Resolvable\n\npublic extension Community {\n    /// Returns a `URL` that can be resolved by another `ApiClient`.\n    func resolvableUrl(from instance: ContentModelUrlType) -> URL {\n        switch instance {\n        case .host: actorId.url\n        case .provider: .community(host: api.host, name: name)\n        }\n    }\n    \n    @inlinable\n    var allResolvableUrls: [URL] {\n        ContentModelUrlType.allCases.map { resolvableUrl(from: $0) }\n    }\n}\n\n// MARK: Codable\n\npublic extension Community {\n    struct CodedData: Codable {\n        let apiUrl: URL\n        let apiMyPersonId: Int?\n        let apiCommunity: LemmyCommunity\n    }\n    \n    internal var apiCommunity: LemmyCommunity {\n        LemmyCommunity(\n            id: id,\n            name: name,\n            title: displayName,\n            description: description,\n            removed: removed,\n            published: created,\n            updated: updated,\n            deleted: deleted,\n            nsfw: nsfw,\n            actorId: actorId,\n            local: apiIsLocal,\n            icon: avatar,\n            banner: banner,\n            hidden: hidden,\n            postingRestrictedToMods: onlyModeratorsCanPost,\n            instanceId: instanceId,\n            visibility: nil,\n            sidebar: nil,\n            publishedAt: created,\n            updatedAt: updated,\n            apId: actorId,\n            lastRefreshedAt: nil,\n            summary: nil,\n            subscribers: nil,\n            posts: nil,\n            comments: nil,\n            usersActiveDay: nil,\n            usersActiveWeek: nil,\n            usersActiveMonth: nil,\n            usersActiveHalfYear: nil,\n            subscribersLocal: nil,\n            reportCount: nil,\n            unresolvedReportCount: nil,\n            localRemoved: nil\n        )\n    }\n    \n    func codedData() async throws -> CodedData {\n        try await .init(\n            apiUrl: api.baseUrl,\n            apiMyPersonId: api.myPersonId,\n            apiCommunity: apiCommunity\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Community/Community+Mock.swift",
    "content": "//\n//  Community+Mock.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-02-17.\n//\n\n// TODO: updated mocks\n//#if DEBUG\n//    public extension Community1 {\n//        static func mock(\n//            api: MockApiClient = .mock,\n//            actorId: ActorIdentifier? = nil,\n//            id: Int,\n//            name: String,\n//            created: Date,\n//            instanceId: Int,\n//            updated: Date?,\n//            displayName: String,\n//            description: String?,\n//            removed: Bool,\n//            deleted: Bool,\n//            nsfw: Bool,\n//            avatar: URL?,\n//            banner: URL?,\n//            hidden: Bool,\n//            onlyModeratorsCanPost: Bool,\n//            blocked: Bool\n//        ) -> Community1 {\n//            .init(\n//                api: api,\n//                actorId: actorId ?? .init(url: URL(string: \"https://\\(api.host)/u/\\(id)\")!)!,\n//                id: id,\n//                name: name,\n//                created: created,\n//                instanceId: instanceId,\n//                updated: updated,\n//                displayName: displayName,\n//                description: description,\n//                removed: removed,\n//                deleted: deleted,\n//                nsfw: nsfw,\n//                avatar: avatar,\n//                banner: banner,\n//                hidden: hidden,\n//                onlyModeratorsCanPost: onlyModeratorsCanPost,\n//                blocked: blocked\n//            )\n//        }\n//    }\n//#endif\n\n//#if DEBUG\n//    public extension Community2 {\n//        static func mock(\n//            community1: Community1,\n//            subscriberCount: Int,\n//            localSubscriberCount: Int,\n//            subscribed: Bool,\n//            subscriptionPending: Bool,\n//            postCount: Int,\n//            commentCount: Int,\n//            activeUserCount: ActiveUserCount,\n//            bannedFromCommunity: Bool?\n//        ) -> Community2 {\n//            .init(\n//                api: community1.api,\n//                community1: community1,\n//                subscription: .init(\n//                    total: subscriberCount,\n//                    local: localSubscriberCount,\n//                    subscribed: subscribed,\n//                    pending: subscriptionPending\n//                ),\n//                postCount: postCount,\n//                commentCount: commentCount,\n//                activeUserCount: activeUserCount,\n//                bannedFromCommunity: bannedFromCommunity\n//            )\n//        }\n//    }\n//#endif\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Community/Community.swift",
    "content": "//\n//  Community.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-02-14.\n//\n\nimport Observation\nimport Foundation\n\npublic enum SubscriptionTier {\n    case unsubscribed, subscribed, favorited\n}\n\n@Observable\npublic final class Community:\n    UnifiedModelProviding,\n    ProfileProviding,\n    CommunityOrPerson,\n    Blockable,\n    ContentIdentifiable,\n    RemovableProviding,\n    PurgableProviding,\n    Sharable,\n    FeedLoadable {\n    public typealias Properties = CommunityProperties\n    \n    public var api: ApiClient\n    private let properties: CommunityProperties\n    @ObservationIgnored lazy var updateQueue: UnifiedUpdateQueue<Community> = .init(parent: self, properties: properties)\n    \n    // MARK: Custom Properties\n    // Mlem-specific properties that are not reflected in the API\n    \n    public var blocked: any RealizedValueProviding<Bool> { blocked_ }\n    public var blocked_: SyntheticRealizedValue<Bool>\n    public var removedPending: Bool = false\n    public var purged: Bool = false\n    /// Used to state-fake internally.\n    public var shouldBeFavorited: Bool = false\n    \n    // MARK: API Properties\n    // Properties that are provided by the API\n    \n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let name: String\n    public let created: Date\n    public let instanceId: Int\n    public var updated: Date?\n    public var displayName: String\n    public var deleted: Bool\n    public var removed: Bool\n    public var nsfw: Bool\n    public var avatar: URL?\n    public var hidden: Bool\n    public var onlyModeratorsCanPost: Bool\n\n    public var description: String?\n    public var banner: URL?\n\n    public var subscription: SyntheticExpectedValue<SubscriptionModel>\n    public var postCount: ExpectedValue<Int>\n    public var commentCount: ExpectedValue<Int>\n    public var activeUserCount: ExpectedValue<ActiveUserCount>\n    public var bannedFromCommunity: ExpectedValue<Bool?>\n    public var instance: ExpectedValue<Instance?>\n    public var moderators: ExpectedValue<[Person]>\n    public var discussionLanguageIds: ExpectedValue<Set<Int>>\n    \n    // MARK: Initializers and Updates\n    \n    public init(api: ApiClient, properties: CommunityProperties) {\n        self.api = api\n        self.properties = properties\n        self.blocked_ = .init(value: api.blocks?.communities.keys.contains(properties.actorId) ?? false, mergeType: .disjunctive)\n        \n        self.actorId = properties.actorId\n        self.id = properties.id\n        self.name = properties.name\n        self.created = properties.created\n        self.instanceId = properties.instanceId\n        self.updated = properties.updated\n        self.displayName = properties.displayName\n        self.deleted = properties.deleted\n        self.removed = properties.removed\n        self.nsfw = properties.nsfw\n        self.avatar = properties.avatar\n        self.hidden = properties.hidden\n        self.onlyModeratorsCanPost = properties.onlyModeratorsCanPost\n\n        // nil-coalesced because PieFed doesn't return these values for some requests.\n        self.description = properties.description ?? nil\n        self.banner = properties.banner ?? nil\n        \n        // because upgrade() is not available until all properties are initialized, first populate all properties\n        // with ExpectedValues that don't actually do anything, then reassign them properly at the end of the init\n        // this is somewhat cumbersome but avoids lazy vars, which are very awkward in Observables\n        self.subscription = dummySyntheticExpectedValue(properties.subscription)\n        self.postCount = dummyExpectedValue(properties.postCount)\n        self.commentCount = dummyExpectedValue(properties.commentCount)\n        self.activeUserCount = dummyExpectedValue(properties.activeUserCount)\n        self.bannedFromCommunity = dummyExpectedValue(properties.bannedFromCommunity)\n        self.instance = dummyExpectedValue(properties.instance)\n        self.moderators = dummyExpectedValue(properties.moderators)\n        self.discussionLanguageIds = dummyExpectedValue(properties.discussionLanguageIds)\n        \n        func expectedValue<T>(_ value: T?) -> ExpectedValue<T> {\n            .init(\n                value: value,\n                provideValue: { try await self.upgrade() })\n        }\n        self.subscription = .init(\n            value: properties.subscription,\n            provideValue: { try await self.upgrade() },\n            mergeType: .disjunctive)\n        self.postCount = expectedValue(properties.postCount)\n        self.commentCount = expectedValue(properties.commentCount)\n        self.activeUserCount = expectedValue(properties.activeUserCount)\n        self.bannedFromCommunity = expectedValue(properties.bannedFromCommunity)\n        self.instance = expectedValue(properties.instance)\n        self.moderators = expectedValue(properties.moderators)\n        self.discussionLanguageIds = expectedValue(properties.discussionLanguageIds)\n        \n        updateAuxiliaryModels(with: properties)\n        self.shouldBeFavorited = favorited\n    }\n    \n    @MainActor\n    public func update(with properties: CommunityProperties) {\n        setIfChanged(\\.updated, properties.updated)\n        setIfChanged(\\.displayName, properties.displayName)\n        setIfChanged(\\.deleted, properties.deleted)\n        setIfChanged(\\.removed, properties.removed)\n        setIfChanged(\\.nsfw, properties.nsfw)\n        setIfChanged(\\.avatar, properties.avatar)\n        setIfChanged(\\.hidden, properties.hidden)\n        setIfChanged(\\.onlyModeratorsCanPost, properties.onlyModeratorsCanPost)\n\n\n        if let description = properties.description {\n            setIfChanged(\\.description, description)\n        }\n        if let banner = properties.banner {\n            setIfChanged(\\.banner, banner)\n        }\n        \n        updateIfChanged(\\.subscription.value_, properties.subscription)\n        updateIfChanged(\\.postCount.value_, properties.postCount)\n        updateIfChanged(\\.commentCount.value_, properties.commentCount)\n        updateIfChanged(\\.activeUserCount.value_, properties.activeUserCount)\n        updateIfChanged(\\.bannedFromCommunity.value_, properties.bannedFromCommunity)\n        setIfNil(\\.instance.value_, properties.instance)\n        updateIfChanged(\\.moderators.value_, properties.moderators)\n        updateIfChanged(\\.discussionLanguageIds.value_, properties.discussionLanguageIds)\n        \n        updateAuxiliaryModels(with: properties)\n        self.shouldBeFavorited = favorited\n    }\n    \n    @MainActor\n    public func softUpdate(with properties: CommunityProperties) {\n        setIfNil(\\.subscription.value_, properties.subscription)\n        setIfNil(\\.postCount.value_, properties.postCount)\n        setIfNil(\\.commentCount.value_, properties.commentCount)\n        setIfNil(\\.activeUserCount.value_, properties.activeUserCount)\n        setIfNil(\\.bannedFromCommunity.value_, properties.bannedFromCommunity)\n        setIfNil(\\.instance.value_, properties.instance)\n        setIfNil(\\.moderators.value_, properties.moderators)\n        setIfNil(\\.discussionLanguageIds.value_, properties.discussionLanguageIds)\n    }\n    \n    /// Updates external models with relevant information from this Community's properties. Should be called in init and update.\n    private func updateAuxiliaryModels(with properties: CommunityProperties) {\n        // if subscription or favorited status changed, update API\n        if properties.subscription != self.subscription.value_ ||\n            favorited != shouldBeFavorited {\n            self.api.subscriptions?.updateCommunitySubscription(community: self)\n        }\n        \n        // if favorited but not subscribed, remove from favorites\n        if favorited, let subscribed = properties.subscription?.subscribed, !subscribed {\n            self.api.subscriptions?.favoriteIDs.remove(id)\n        }\n        \n        // if banned, update ban status\n        if let bannedFromCommunity = properties.bannedFromCommunity as? Bool {\n            api.myPerson?.updateKnownCommunityBanState(id: id, banned: bannedFromCommunity)\n        }\n    }\n    \n    // MARK: Upgrades\n    \n    public func upgrade() async throws {\n        try await updateQueue.upgrade()\n    }\n    \n    public func refresh() async throws {\n        try await updateQueue.refresh()\n    }\n    \n    public func fetchUpgraded() async throws -> CommunityProperties {\n        let snapshot = try await api.repository.getCommunity(id: id)\n        return await .init(api: api, snapshot: .community3(snapshot))\n    }\n    \n    public func resolve(with api: ApiClient) async throws -> Self {\n        let stub = CommunityStub(api: api, url: allResolvableUrls[0])\n        return try await stub.getCommunity() as! Self\n    }\n}\n\n// MARK: Computed\n\npublic extension Community {\n    var favorited: Bool {\n        api.subscriptions?.isFavorited(self) ?? false\n    }\n    \n    /// - Note: will trigger fetch if subscription value not present\n    var subscriptionTier: SubscriptionTier {\n        if favorited { return .favorited }\n        if subscription.value?.subscribed ?? false { return .subscribed }\n        return .unsubscribed\n    }\n}\n\n// MARK: Interactions\n\npublic extension Community {\n    \n    // Get Posts\n    \n    func getPosts(\n        sort: PostSortType,\n        page: Int = 1,\n        cursor: String? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil,\n        showHidden: Bool = false\n    ) async throws -> (posts: [Post], cursor: String?) {\n        try await api.getPosts(\n            communityId: id,\n            sort: sort,\n            page: page,\n            cursor: cursor,\n            limit: limit,\n            filter: filter,\n            showHidden: showHidden\n        )\n    }\n    \n    // Subscribe\n    \n    var updateSubscribed: ((Bool) -> Void)? {\n        if let subscription = subscription.value {\n            return { self.updateSubscribed($0, subscription: subscription) }\n        }\n        return nil\n    }\n    \n    private func updateSubscribed(_ newValue: Bool, subscription: SubscriptionModel) {\n        self.subscription.value_ = subscription.withSubscriptionStatus(subscribed: newValue, isLocal: apiIsLocal)\n        let oldFavorited = shouldBeFavorited\n        if !newValue {\n            self.shouldBeFavorited = false\n        }\n        \n        Task {\n            await updateQueue.addItem {\n                do {\n                    let snapshot = try await self.api.repository.subscribeToCommunity(id: self.id, subscribe: newValue)\n                    return await .init(api: self.api, snapshot: .community2(snapshot))\n                } catch {\n                    self.shouldBeFavorited = oldFavorited\n                    throw error\n                }\n            }\n        }\n    }\n    \n    // Favorite\n    \n    var updateFavorite: ((Bool) -> Void)? {\n        if let subscription = subscription.value {\n            return { self.updateFavorite($0, subscription: subscription) }\n        }\n        return nil\n    }\n    \n    private func updateFavorite(_ newValue: Bool, subscription: SubscriptionModel) {\n        self.shouldBeFavorited = newValue\n        if !subscription.subscribed, newValue {\n            // if not subscribed already, subscribe. This automatically updates favorites tracked in ApiClient\n            updateSubscribed(true, subscription: subscription)\n        } else {\n            // if already subscribed, just update favorites tracked in ApiClient\n            api.subscriptions?.updateCommunitySubscription(community: self)\n        }\n    }\n    \n    // Remove\n    \n    func updateRemoved(_ newValue: Bool, reason: String?, callback: ((UpdateStatus) -> Void)?) {\n        removed = newValue\n        removedPending = true\n        \n        Task {\n            await updateQueue.addItem {\n                do {\n                    let snapshot = try await self.api.repository.removeCommunity(id: self.id, remove: newValue, reason: reason)\n                    callback?(.success)\n                    return await .init(api: self.api, snapshot: .community2(snapshot))\n                } catch {\n                    callback?(.failure(error))\n                    throw (error)\n                }\n            }\n        }\n    }\n    \n    // Purge\n    \n    func purge(reason: String?) async throws {\n        try await api.purgeCommunity(id: id, reason: reason)\n        purged = true\n    }\n    \n    // Edit Moderators\n    \n    func addModerator(personId: Int, added: Bool) {\n        Task {\n            await updateQueue.addItem { properties in\n                var properties = properties\n                let snapshots = try await self.api.repository.addModerator(communityId: self.id, personId: personId, added: added)\n                \n                if added {\n                    guard snapshots.moderators.contains(where: { $0.id == personId }) else {\n                        throw ApiClientError.unsuccessful\n                    }\n                } else {\n                    guard !snapshots.moderators.contains(where: { $0.id == personId }) else {\n                        throw ApiClientError.unsuccessful\n                    }\n                }\n                \n                let newModerators = await self.api.caches.person.getModels(api: self.api, from: snapshots.moderators.map { .person1($0) })\n                \n                // update new moderator\n                if let person = self.api.caches.person.retrieveModel(cacheId: personId) {\n                    await person.updateQueue.addItem { personProperties in\n                        var personProperties = personProperties\n                        var moderatedCommunities: [Community] = personProperties.moderatedCommunities ?? .init()\n                        if added {\n                            moderatedCommunities.append(self)\n                        } else {\n                            moderatedCommunities.removeAll(where: { $0.id == self.id })\n                        }\n                        personProperties.moderatedCommunities = moderatedCommunities\n                        return personProperties\n                    }\n                }\n                \n                properties.moderators = newModerators\n                return properties\n            }\n        }\n    }\n    \n    func addModerator(_ person: Person, added: Bool) async throws {\n        addModerator(personId: person.id, added: added)\n    }\n    \n    // Description\n\n    func updateDescription(_ newValue: String?, callback: ((UpdateStatus) -> Void)?) {\n        description = newValue\n        \n        Task {\n            await updateQueue.addItem {\n                do {\n                    let ret: CommunityProperties = try await .init(\n                        api: self.api,\n                        snapshot: .community2(self.api.repository.editCommunityDescription(id: self.id, newValue: newValue)))\n                    callback?(.success)\n                    return ret\n                } catch {\n                    callback?(.failure(error))\n                    throw(error)\n                }\n            }\n        }\n    }\n}\n\n// MARK: Shim\n\npublic extension Community {\n    var displayName_: String { displayName }\n    var description_: String? { description }\n    var banner_: URL? { banner }\n    var created_: Date { created }\n    var updated_: Date? { updated }\n    \n    internal func takeSnapshot1() -> Community1Snapshot {\n        .init(actorId: actorId,\n              id: id,\n              name: name,\n              created: created,\n              instanceId: instanceId,\n              updated: updated,\n              displayName: displayName,\n              description: description,\n              deleted: deleted,\n              removed: removed,\n              nsfw: nsfw,\n              avatar: avatar,\n              banner: banner,\n              hidden: hidden,\n              onlyModeratorsCanPost: onlyModeratorsCanPost,\n              allPropertiesPresent: false\n        )\n    }\n}\n\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Community/CommunityProperties.swift",
    "content": "//\n//  CommunityProperties.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-02-14.\n//\n\nimport Foundation\n\npublic struct CommunityProperties: UnifiedPropertiesProviding {\n    // From Community1Snapshot, guaranteed to always be present\n    let actorId: ActorIdentifier\n    let id: Int\n    let name: String\n    let created: Date\n    let instanceId: Int\n    var updated: Date?\n    var displayName: String\n    var deleted: Bool\n    var removed: Bool\n    var nsfw: Bool\n    var avatar: URL?\n    var hidden: Bool\n    var onlyModeratorsCanPost: Bool\n\n    // From Community1Snapshot, but PieFed does not always provide these\n    // https://codeberg.org/rimu/pyfedi/issues/882\n    var banner: URL??\n    var description: String??\n    \n    // From Community2Snapshot\n    var subscription: SubscriptionModel?\n    var postCount: Int?\n    var commentCount: Int?\n    var activeUserCount: ActiveUserCount?\n    var bannedFromCommunity: Bool??\n    \n    // From Community3Snapshot\n    var instance: Instance??\n    var moderators: [Person]?\n    var discussionLanguageIds: Set<Int>?\n    \n    @MainActor\n    public init(api: ApiClient, snapshot: AnyCommunitySnapshot) {\n        let snapshot1: Community1Snapshot\n        let snapshot2: Community2Snapshot?\n        let snapshot3: Community3Snapshot?\n        switch snapshot {\n        case let .community1(snapshot):\n            snapshot1 = snapshot\n            snapshot2 = nil\n            snapshot3 = nil\n        case let .community2(snapshot):\n            snapshot1 = snapshot.community\n            snapshot2 = snapshot\n            snapshot3 = nil\n        case let .community3(snapshot):\n            snapshot1 = snapshot.community.community\n            snapshot2 = snapshot.community\n            snapshot3 = snapshot\n        }\n        \n        if let snapshot3 {\n            if let instance1Snapshot = snapshot3.instance {\n                instance = api.caches.instance.getOptionalModel(api: api, from: .instance1(instance1Snapshot))\n            } else {\n                instance = nil\n            }\n            moderators = api.caches.person.getModels(api: api, from: snapshot3.moderators.map { .person1($0) })\n            discussionLanguageIds = snapshot3.discussionLanguageIds\n        }\n        \n        if let snapshot2 {\n            subscription = snapshot2.subscription\n            postCount = snapshot2.postCount\n            commentCount = snapshot2.commentCount\n            activeUserCount = snapshot2.activeUserCount\n            bannedFromCommunity = snapshot2.bannedFromCommunity\n        }\n        \n        actorId = snapshot1.actorId\n        id = snapshot1.id\n        name = snapshot1.name\n        created = snapshot1.created\n        instanceId = snapshot1.instanceId\n        updated = snapshot1.updated\n        displayName = snapshot1.displayName\n        deleted = snapshot1.deleted\n        removed = snapshot1.removed\n        nsfw = snapshot1.nsfw\n        avatar = snapshot1.avatar\n        hidden = snapshot1.hidden\n        onlyModeratorsCanPost = snapshot1.onlyModeratorsCanPost\n\n        if snapshot1.allPropertiesPresent {\n            banner = snapshot1.banner\n            description = snapshot1.description\n        }\n    }\n    \n    public mutating func merge(_ other: CommunityProperties) {\n        // tier 1 properties: simple assignment\n        self.updated = other.updated\n        self.displayName = other.displayName\n        self.deleted = other.deleted\n        self.removed = other.removed\n        self.nsfw = other.nsfw\n        self.avatar = other.avatar\n        self.hidden = other.hidden\n        self.onlyModeratorsCanPost = other.onlyModeratorsCanPost\n        \n        // tier 2, 3 properties: only assign if incoming non-nil\n        self.description = other.description ?? self.description\n        self.banner = other.banner ?? self.banner\n\n        self.subscription = other.subscription ?? self.subscription\n        self.postCount = other.postCount ?? self.postCount\n        self.commentCount = other.commentCount ?? self.commentCount\n        self.activeUserCount = other.activeUserCount ?? self.activeUserCount\n        self.bannedFromCommunity = other.bannedFromCommunity ?? self.bannedFromCommunity\n        \n        self.instance = other.instance ?? self.instance\n        self.moderators = other.moderators ?? self.moderators\n        self.discussionLanguageIds = other.discussionLanguageIds ?? self.discussionLanguageIds\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Community/CommunityStub.swift",
    "content": "//\n//  CommunityStub.swift\n//  Mlem\n//\n//  Created by Sjmarf on 03/02/2024.\n//\n\nimport Foundation\n\npublic struct CommunityStub:  Hashable {\n    public var api: ApiClient\n    public let url: URL\n    \n    public init(api: ApiClient, url: URL) {\n        self.api = api\n        self.url = url\n    }\n    \n    public func asLocal() -> Self {\n        .init(api: .getApiClient(url: url.removingPathComponents(), username: nil), url: url)\n    }\n    \n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(url)\n    }\n    \n    public static func == (lhs: CommunityStub, rhs: CommunityStub) -> Bool {\n        lhs.url == rhs.url\n    }\n    \n    public func getCommunity() async throws -> Community {\n        try await api.getCommunity(url: url)\n    }\n}\n\n// Resolvable conformance\npublic extension CommunityStub {\n    var resolvableUrl: URL { url }\n    \n    @inlinable\n    var allResolvableUrls: [URL] { [resolvableUrl] }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/CommunityOrPersonStub.swift",
    "content": "//\n//  CommunityOrAccount.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/02/2024.\n//\n\nimport Foundation\nimport Observation\n\npublic protocol CommunityOrPerson: ContentModel, ActorIdentifiable {\n    static var identifierPrefix: String { get }\n    \n    var name: String { get }\n}\n\npublic extension CommunityOrPerson {\n    var fullName: String { \"\\(name)@\\(host)\" }\n    \n    var fullNameWithPrefix: String { \"\\(Self.identifierPrefix)\\(name)@\\(host)\" }\n}\n\n\npublic protocol Blockable: ActorIdentifiable {\n    /// Whether the entity knows itself to be blocked.\n    /// - Note: Some types (e.g., `InstanceSummary`) do not track blocked status. For the most accurate blocked status, use\n    /// `blocked(environment: EnvironmentValues)` as defined in Mlem\n    /// - Warning: there is a Swift compiler bug that causes compilation to fail if you reference `blocked.realizedValue` in\n    /// certain contexts. It is recommended to use `blocked_.realizedValue` any time you are working with a concrete type.\n    var blocked: any RealizedValueProviding<Bool> { get }\n\n    /// Updates the blocked status to the given value\n    /// - Parameters:\n    ///   - newValue: intended block status\n    ///   - callback: if present, will be called when the block completes with true if the update succeeds and false otherwise.\n    var updateBlocked: ((Bool, ((Bool) -> Void)?) -> Void)? { get }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ContentModel.swift",
    "content": "//\n//  ContentModel.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/02/2024.\n//\n\nimport Foundation\n\npublic protocol ContentModel {\n    var api: ApiClient { get }\n}\n\nextension ContentModel {\n    @MainActor\n    func setIfChanged<T: Equatable>(_ keyPath: ReferenceWritableKeyPath<Self, T>, _ value: T) {\n        if self[keyPath: keyPath] != value {\n            self[keyPath: keyPath] = value\n        }\n    }\n}\n\npublic extension ContentModel where Self: ActorIdentifiable {\n    var apiIsLocal: Bool {\n        api.host == \"localhost\" || api.host == host\n    }\n}\n\npublic protocol ContentIdentifiable: AnyObject, ContentModel, Hashable, Identifiable where ID == Int {\n    static var modelTypeId: ContentType { get }\n}\n\npublic extension ContentIdentifiable {\n    var uid: Int {\n        var hasher = Hasher()\n        hasher.combine(Self.modelTypeId)\n        hasher.combine(id)\n        return hasher.finalize()\n    }\n}\n\npublic extension ContentIdentifiable {\n    func hash(into hasher: inout Hasher) {\n        hasher.combine(api)\n        hasher.combine(id)\n        hasher.combine(Self.modelTypeId)\n    }\n    \n    static func == (lhs: Self, rhs: Self) -> Bool {\n        lhs === rhs\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ContentModelUrlType.swift",
    "content": "//\n//  ContentModelUrlType.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-01-26.\n//\n\nimport Foundation\n\npublic enum ContentModelUrlType: CaseIterable {\n    /// Refers to the instance that this entity originally came from.\n    case host\n    /// Refers to the instance that provided this entity (e.g. the `ApiClient` attached to the entity).\n    case provider\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ContentType.swift",
    "content": "//\n//  Content Type.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-08-26.\n//\n\nimport Foundation\n\npublic enum ContentType: Int, Codable {\n    case post, comment, community, person, message, mention, reply, instance, registrationApplication\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/DeletableProviding.swift",
    "content": "//\n//  DeletableProviding.swift\n//\n//\n//  Created by Sjmarf on 22/07/2024.\n//\n\nimport Foundation\n\npublic protocol DeletableProviding: OwnershipProviding {\n    var deleted: Bool { get }\n    \n    func updateDeleted(_ newValue: Bool, callback: ((UpdateStatus) -> Void)?)\n}\n\npublic extension DeletableProviding {\n    func toggleDeleted(callback: ((UpdateStatus) -> Void)? = nil) {\n        updateDeleted(!deleted, callback: callback)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Feature.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic enum Feature: Hashable {\n    case postSortType(PostSortType)\n    case commentSortType(CommentSortType)\n    case searchSortType(SearchSortType)\n    case sortTimeRange(SortTimeRange)\n    case listingType(ListingType)\n    \n    case viewVotes\n    \n    case hidePosts\n    case searchLocalPeople\n    case searchLocalCommunities\n    case searchLocalComments\n\n    case modlog\n    case viewInstanceCreationDate\n    case viewInstanceSettings\n    case viewCommunityActiveUsers\n    \n    case logIn\n    case signUp\n    \n    case viewReports\n    case viewMentionsAndPrivateMessages\n    \n    case editAndDeletePrivateMessages\n    case undeletePrivateMessages\n    case reportPrivateMessages\n    case purgeContent\n    case removeCommunity\n    case banFromInstance\n    \n    case banFromCommunity\n    case banFromNonLocalCommunity\n    \n    case unbanWithReason\n    \n    /// Add/remove moderators from a community\n    case editModeratorList\n    case editCommunityDescription\n    \n    case uploadImages\n    case commentSearch\n\n    case editProfile\n    case editAccountSettings\n    case editDisplayName\n    \n    /// Server automatically marks posts as read when voted on or saved\n    case autoMarkPostReadOnInteract\n    \n    case blockInstances\n    case viewInstanceBlockList\n    case moderatorSetNsfw\n    \n    case fetchLinkMetadata\n    case customPostThumbnail\n\n    case userNotes\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/FederationPolicy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic struct FederationPolicy {\n    let allowed: Set<String>\n    let blocked: Set<String>\n    \n    init(from federatedInstances: LemmyFederatedInstances) {\n        self.allowed = Set(federatedInstances.allowed.map(\\.domain))\n        self.blocked = Set(federatedInstances.blocked.map(\\.domain))\n    }\n    \n    init(from instances: [LemmyFederatedInstanceView]) {\n        var allowed: Set<String> = []\n        var blocked: Set<String> = []\n        for instance in instances {\n            if instance.allowed != nil {\n                allowed.insert(instance.instance.domain)\n            }\n            if instance.blocked != nil {\n                blocked.insert(instance.instance.domain)\n            }\n        }\n        self.allowed = allowed\n        self.blocked = blocked\n    }\n}\n\npublic enum FederationMode: Hashable {\n    case all, local, disable\n}\n\npublic struct VoteFederationMode: Hashable {\n    public let postUpvote: FederationMode\n    public let postDownvote: FederationMode\n    public let commentUpvote: FederationMode\n    public let commentDownvote: FederationMode\n\n    public static let all: Self = .init(\n        postUpvote: .all,\n        postDownvote: .all,\n        commentUpvote: .all,\n        commentDownvote: .all\n    )\n\n    public static let downvotesDisabled: Self = .init(\n        postUpvote: .all,\n        postDownvote: .disable,\n        commentUpvote: .all,\n        commentDownvote: .disable\n    )\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/FederationStatus.swift",
    "content": "//\n//  FederationStatus.swift\n//\n//\n//  Created by Sjmarf on 10/06/2024.\n//\n\nimport Foundation\n\npublic enum FederationStatus {\n    case explicitlyAllowed, explicitlyBlocked, implicitlyAllowed, implicitlyBlocked\n    \n    public var isExplicit: Bool {\n        self == .explicitlyAllowed || self == .explicitlyBlocked\n    }\n    \n    public var isAllowed: Bool {\n        self == .explicitlyAllowed || self == .implicitlyAllowed\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/GetContentFilter.swift",
    "content": "//\n//  GetContentFilter.swift\n//\n//\n//  Created by Sjmarf on 24/06/2024.\n//\n\nimport Foundation\n\npublic enum GetContentFilter {\n    case saved, upvoted, downvoted\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ImageUpload/ImageUpload1.swift",
    "content": "//\n//  ImageUpload1.swift\n//\n//\n//  Created by Sjmarf on 26/08/2024.\n//\n\nimport Foundation\nimport Observation\n\n// There are no higher tiers of this model yet - in future `ImageUpload2` will be\n// created from `LemmyLocalImage` and `ImageUpload3` will be created from `LemmyLocalImageView`.\n\n@Observable\npublic class ImageUpload1: ImageUpload1Providing {\n    public var api: ApiClient\n    public var mediaUpload1: ImageUpload1 { self }\n    \n    public let url: URL\n    \n    // This includes the file extension\n    let alias: String?\n    let deleteToken: String?\n    \n    public internal(set) var deleted: Bool = false\n    \n    init(api: ApiClient, url: URL, alias: String?, deleteToken: String?) {\n        self.api = api\n        self.url = url\n        self.alias = alias\n        self.deleteToken = deleteToken\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ImageUpload/ImageUpload1Providing.swift",
    "content": "//\n//  ImageUpload1Providing.swift\n//\n//\n//  Created by Sjmarf on 26/08/2024.\n//\n\nimport Foundation\n\npublic protocol ImageUpload1Providing: ContentModel, Hashable {\n    var mediaUpload1: ImageUpload1 { get }\n    var url: URL { get }\n    var deleted: Bool { get }\n}\n\npublic extension ImageUpload1Providing {\n    /// Delete the image. Doesn't state-fake. Can't be undone.\n    func delete() async throws {\n        guard let alias = mediaUpload1.alias, let deleteToken = mediaUpload1.deleteToken else {\n            throw ApiClientError.featureUnsupported\n        }\n        try await api.deleteImage(alias: alias, deleteToken: deleteToken)\n        mediaUpload1.deleted = true\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(url)\n    }\n    \n    static func == (lhs: Self, rhs: Self) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n\npublic typealias ImageUpload = ImageUpload1Providing\n \n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/InboxItemProviding.swift",
    "content": "//\n//  InboxItemProviding.swift\n//\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport Foundation\n\npublic protocol InboxItemProviding: ContentIdentifiable, ContentModel, ReadableProviding {\n    var created: Date { get }\n    var read: Bool { get }\n    \n    @discardableResult\n    func updateRead(_ newValue: Bool) -> Task<StateUpdateResult, Never>\n}\n\npublic extension InboxItemProviding {\n    @discardableResult\n    func toggleRead() -> Task<StateUpdateResult, Never> {\n        updateRead(!read)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Instance/Instance+Conformance.swift",
    "content": "//\n//  Instance+Conformance.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-03-13.\n//\n\nimport Foundation\n\n// MARK: CacheIdentifiable\n\npublic extension Instance {\n    var cacheId: Int { id }\n}\n\n// MARK: ContentIdentifiable\n\npublic extension Instance {\n    static var modelTypeId: ContentType { .instance }\n}\n\n// MARK: ProfileProviding\n\npublic extension Instance {\n    var profileCreated: Date? { created }\n}\n\n// MARK: Blockable\n\npublic extension Instance {\n    var updateBlocked: ((Bool, ((Bool) -> Void)?) -> Void)? {\n        self.api.token == nil ? nil : self._updateBlocked\n    }\n    \n    private func _updateBlocked(_ newValue: Bool, callback: ((Bool) -> Void)? = nil) {\n        let oldValue = blocked.realizedValue\n        blocked_.set(newValue)\n        \n        Task {\n            await updateQueue.addItem { properties in\n                do {\n                    try await self.api.repository.blockInstance(instanceId: self.instanceId, block: newValue)\n                    callback?(true)\n                    if newValue {\n                        self.api.blocks?.instances[self.actorId] = self.instanceId\n                    } else {\n                        self.api.blocks?.instances.removeValue(forKey: self.actorId)\n                    }\n                    return properties\n                } catch {\n                    self.blocked_.set(oldValue)\n                    callback?(false)\n                    throw error\n                }\n            }\n        }\n    }\n}\n\n// MARK: Sharable\n\npublic extension Instance {\n    func url() -> URL {\n        actorId.url\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Instance/Instance.swift",
    "content": "//\n//  Instance.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-03-13.\n//\n\nimport Observation\nimport Foundation\nimport os\n\n@Observable\npublic final class Instance:\n    UnifiedModelProviding,\n    ActorIdentifiable,\n    Blockable,\n    ProfileProviding,\n    ContentIdentifiable,\n    Sharable {\n    public typealias Properties = InstanceProperties\n    \n    public var api: ApiClient\n    private let properties: InstanceProperties\n    @ObservationIgnored lazy var updateQueue: UnifiedUpdateQueue<Instance> = .init(parent: self, properties: properties)\n    \n    // MARK: Custom Properties\n    // Mlem-specific properties that are not reflected in the API\n    \n    public var blocked: any RealizedValueProviding<Bool> { blocked_ }\n    public var blocked_: RealizedValue<Bool>\n    \n    /// If this is `false`, The instance is *not* guaranteed to be non-local, particularly for locally running instances.\n    public var local: Bool = false\n    \n    // MARK: API Properties\n    // Properties that are provided by the API\n    \n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let instanceId: Int\n    public let created: Date\n    public let updated: Date?\n    public let publicKey: String\n    public var displayName: String\n    public var description: String?\n    public var shortDescription: String?\n    public var avatar: URL?\n    public var banner: URL?\n    public var lastRefresh: Date\n    public var contentWarning: String?\n    \n    public var setup: ExpectedValue<Bool>\n    public var voteFederationMode: ExpectedValue<VoteFederationMode>\n    public var nsfwContentEnabled: ExpectedValue<Bool>\n    public var communityCreationRestrictedToAdmins: ExpectedValue<Bool>\n    public var emailVerificationRequired: ExpectedValue<Bool>\n    public var applicationQuestion: ExpectedValue<String?>\n    public var isPrivate: ExpectedValue<Bool>\n    public var defaultTheme: ExpectedValue<String>\n    public var defaultFeed: ExpectedValue<ListingType>\n    public var legalInformation: ExpectedValue<String?>\n    public var hideModlogNames: ExpectedValue<Bool>\n    public var emailApplicationsToAdmins: ExpectedValue<Bool>\n    public var emailReportsToAdmins: ExpectedValue<Bool>\n    public var slurFilterRegex: ExpectedValue<String?>\n    public var actorNameMaxLength: ExpectedValue<Int>\n    public var federationEnabled: ExpectedValue<Bool>\n    public var captchaEnabled: ExpectedValue<Bool>\n    public var captchaDifficulty: ExpectedValue<CaptchaDifficulty?>\n    public var registrationMode: ExpectedValue<RegistrationMode>\n    public var federationSignedFetch: ExpectedValue<Bool?>\n    public var defaultPostListingMode: ExpectedValue<PostFeedViewMode?>\n    public var defaultPostSortType: ExpectedValue<PostSortType?>\n    public var userCount: ExpectedValue<Int>\n    public var postCount: ExpectedValue<Int>\n    public var commentCount: ExpectedValue<Int>\n    public var communityCount: ExpectedValue<Int>\n    public var activeUserCount: ExpectedValue<ActiveUserCount>\n    \n    public var allLanguages: ExpectedValue<[Locale.Language]>\n    public var software: ExpectedValue<SiteSoftware>\n    public var allowedLanguageIds: ExpectedValue<Set<Int>>\n    public var blockedUrls: ExpectedValue<[InstanceUrlBlockRecord]?>\n    public var administrators: ExpectedValue<[Person]>\n    \n    public init(api: ApiClient, properties: InstanceProperties) {\n        self.api = api\n        self.properties = properties\n        self.blocked_ = .init(api.blocks?.instances.keys.contains(properties.actorId) ?? false)\n        \n        self.actorId = properties.actorId\n        self.id = properties.id\n        self.instanceId = properties.instanceId\n        self.created = properties.created\n        self.updated = properties.updated\n        self.publicKey = properties.publicKey\n        self.displayName = properties.displayName\n        self.description = properties.description\n        self.shortDescription = properties.shortDescription\n        self.avatar = properties.avatar\n        self.banner = properties.banner\n        self.lastRefresh = properties.lastRefresh\n        self.contentWarning = properties.contentWarning\n        \n        // because upgrade() is not available until all properties are initialized, first populate all properties\n        // with ExpectedValues that don't actually do anything, then reassign them properly at the end of the init\n        // this is somewhat cumbersome but avoids lazy vars, which are very awkward in Observables\n        self.setup = dummyExpectedValue(properties.setup)\n        self.voteFederationMode = dummyExpectedValue(properties.voteFederationMode)\n        self.nsfwContentEnabled = dummyExpectedValue(properties.nsfwContentEnabled)\n        self.communityCreationRestrictedToAdmins = dummyExpectedValue(properties.communityCreationRestrictedToAdmins)\n        self.emailVerificationRequired = dummyExpectedValue(properties.emailVerificationRequired)\n        self.applicationQuestion = dummyExpectedValue(properties.applicationQuestion)\n        self.isPrivate = dummyExpectedValue(properties.isPrivate)\n        self.defaultTheme = dummyExpectedValue(properties.defaultTheme)\n        self.defaultFeed = dummyExpectedValue(properties.defaultFeed)\n        self.legalInformation = dummyExpectedValue(properties.legalInformation)\n        self.hideModlogNames = dummyExpectedValue(properties.hideModlogNames)\n        self.emailApplicationsToAdmins = dummyExpectedValue(properties.emailApplicationsToAdmins)\n        self.emailReportsToAdmins = dummyExpectedValue(properties.emailReportsToAdmins)\n        self.slurFilterRegex = dummyExpectedValue(properties.slurFilterRegex)\n        self.actorNameMaxLength = dummyExpectedValue(properties.actorNameMaxLength)\n        self.federationEnabled = dummyExpectedValue(properties.federationEnabled)\n        self.captchaEnabled = dummyExpectedValue(properties.captchaEnabled)\n        self.captchaDifficulty = dummyExpectedValue(properties.captchaDifficulty)\n        self.registrationMode = dummyExpectedValue(properties.registrationMode)\n        self.federationSignedFetch = dummyExpectedValue(properties.federationSignedFetch)\n        self.defaultPostListingMode = dummyExpectedValue(properties.defaultPostListingMode)\n        self.defaultPostSortType = dummyExpectedValue(properties.defaultPostSortType)\n        self.userCount = dummyExpectedValue(properties.userCount)\n        self.postCount = dummyExpectedValue(properties.postCount)\n        self.commentCount = dummyExpectedValue(properties.commentCount)\n        self.communityCount = dummyExpectedValue(properties.communityCount)\n        self.activeUserCount = dummyExpectedValue(properties.activeUserCount)\n        self.allLanguages = dummyExpectedValue(properties.allLanguages)\n        self.software = dummyExpectedValue(properties.software)\n        self.allowedLanguageIds = dummyExpectedValue(properties.allowedLanguageIds)\n        self.blockedUrls = dummyExpectedValue(properties.blockedUrls)\n        self.administrators = dummyExpectedValue(properties.administrators)\n        \n        func expectedValue<T>(_ value: T?) -> ExpectedValue<T> {\n            .init(\n                value: value,\n                provideValue: { try await self.upgrade() }\n            )\n        }\n        self.setup = expectedValue(properties.setup)\n        self.voteFederationMode = expectedValue(properties.voteFederationMode)\n        self.nsfwContentEnabled = expectedValue(properties.nsfwContentEnabled)\n        self.communityCreationRestrictedToAdmins = expectedValue(properties.communityCreationRestrictedToAdmins)\n        self.emailVerificationRequired = expectedValue(properties.emailVerificationRequired)\n        self.applicationQuestion = expectedValue(properties.applicationQuestion)\n        self.isPrivate = expectedValue(properties.isPrivate)\n        self.defaultTheme = expectedValue(properties.defaultTheme)\n        self.defaultFeed = expectedValue(properties.defaultFeed)\n        self.legalInformation = expectedValue(properties.legalInformation)\n        self.hideModlogNames = expectedValue(properties.hideModlogNames)\n        self.emailApplicationsToAdmins = expectedValue(properties.emailApplicationsToAdmins)\n        self.emailReportsToAdmins = expectedValue(properties.emailReportsToAdmins)\n        self.slurFilterRegex = expectedValue(properties.slurFilterRegex)\n        self.actorNameMaxLength = expectedValue(properties.actorNameMaxLength)\n        self.federationEnabled = expectedValue(properties.federationEnabled)\n        self.captchaEnabled = expectedValue(properties.captchaEnabled)\n        self.captchaDifficulty = expectedValue(properties.captchaDifficulty)\n        self.registrationMode = expectedValue(properties.registrationMode)\n        self.federationSignedFetch = expectedValue(properties.federationSignedFetch)\n        self.defaultPostListingMode = expectedValue(properties.defaultPostListingMode)\n        self.defaultPostSortType = expectedValue(properties.defaultPostSortType)\n        self.userCount = expectedValue(properties.userCount)\n        self.postCount = expectedValue(properties.postCount)\n        self.commentCount = expectedValue(properties.commentCount)\n        self.communityCount = expectedValue(properties.communityCount)\n        self.activeUserCount = expectedValue(properties.activeUserCount)\n        \n        self.allLanguages = expectedValue(properties.allLanguages)\n        self.software = expectedValue(properties.software)\n        self.allowedLanguageIds = expectedValue(properties.allowedLanguageIds)\n        self.blockedUrls = expectedValue(properties.blockedUrls)\n        self.administrators = expectedValue(properties.administrators)\n    }\n    \n    @MainActor\n    public func update(with properties: InstanceProperties) {\n        setIfChanged(\\.displayName, properties.displayName)\n        setIfChanged(\\.description, properties.description)\n        setIfChanged(\\.shortDescription, properties.shortDescription)\n        setIfChanged(\\.avatar, properties.avatar)\n        setIfChanged(\\.banner, properties.banner)\n        setIfChanged(\\.lastRefresh, properties.lastRefresh)\n        setIfChanged(\\.contentWarning, properties.contentWarning)\n        \n        updateIfChanged(\\.setup.value_, properties.setup)\n        updateIfChanged(\\.voteFederationMode.value_, properties.voteFederationMode)\n        updateIfChanged(\\.nsfwContentEnabled.value_, properties.nsfwContentEnabled)\n        updateIfChanged(\\.communityCreationRestrictedToAdmins.value_, properties.communityCreationRestrictedToAdmins)\n        updateIfChanged(\\.emailVerificationRequired.value_, properties.emailVerificationRequired)\n        updateIfChanged(\\.applicationQuestion.value_, properties.applicationQuestion)\n        updateIfChanged(\\.isPrivate.value_, properties.isPrivate)\n        updateIfChanged(\\.defaultTheme.value_, properties.defaultTheme)\n        updateIfChanged(\\.defaultFeed.value_, properties.defaultFeed)\n        updateIfChanged(\\.legalInformation.value_, properties.legalInformation)\n        updateIfChanged(\\.hideModlogNames.value_, properties.hideModlogNames)\n        updateIfChanged(\\.emailApplicationsToAdmins.value_, properties.emailApplicationsToAdmins)\n        updateIfChanged(\\.emailReportsToAdmins.value_, properties.emailReportsToAdmins)\n        updateIfChanged(\\.slurFilterRegex.value_, properties.slurFilterRegex)\n        updateIfChanged(\\.actorNameMaxLength.value_, properties.actorNameMaxLength)\n        updateIfChanged(\\.federationEnabled.value_, properties.federationEnabled)\n        updateIfChanged(\\.captchaEnabled.value_, properties.captchaEnabled)\n        updateIfChanged(\\.captchaDifficulty.value_, properties.captchaDifficulty)\n        updateIfChanged(\\.registrationMode.value_, properties.registrationMode)\n        updateIfChanged(\\.federationSignedFetch.value_, properties.federationSignedFetch)\n        updateIfChanged(\\.defaultPostListingMode.value_, properties.defaultPostListingMode)\n        updateIfChanged(\\.defaultPostSortType.value_, properties.defaultPostSortType)\n        updateIfChanged(\\.userCount.value_, properties.userCount)\n        updateIfChanged(\\.postCount.value_, properties.postCount)\n        updateIfChanged(\\.commentCount.value_, properties.commentCount)\n        updateIfChanged(\\.communityCount.value_, properties.communityCount)\n        updateIfChanged(\\.activeUserCount.value_, properties.activeUserCount)\n        \n        setIfNil(\\.allLanguages.value_, properties.allLanguages) // not expected to change\n        updateIfChanged(\\.software.value_, properties.software)\n        updateIfChanged(\\.allowedLanguageIds.value_, properties.allowedLanguageIds)\n        updateIfChanged(\\.blockedUrls.value_, properties.blockedUrls)\n        updateIfChanged(\\.administrators.value_, properties.administrators)\n    }\n    \n    @MainActor\n    public func softUpdate(with properties: InstanceProperties) {\n        setIfNil(\\.setup.value_, properties.setup)\n        setIfNil(\\.voteFederationMode.value_, properties.voteFederationMode)\n        setIfNil(\\.nsfwContentEnabled.value_, properties.nsfwContentEnabled)\n        setIfNil(\\.communityCreationRestrictedToAdmins.value_, properties.communityCreationRestrictedToAdmins)\n        setIfNil(\\.emailVerificationRequired.value_, properties.emailVerificationRequired)\n        setIfNil(\\.applicationQuestion.value_, properties.applicationQuestion)\n        setIfNil(\\.isPrivate.value_, properties.isPrivate)\n        setIfNil(\\.defaultTheme.value_, properties.defaultTheme)\n        setIfNil(\\.defaultFeed.value_, properties.defaultFeed)\n        setIfNil(\\.legalInformation.value_, properties.legalInformation)\n        setIfNil(\\.hideModlogNames.value_, properties.hideModlogNames)\n        setIfNil(\\.emailApplicationsToAdmins.value_, properties.emailApplicationsToAdmins)\n        setIfNil(\\.emailReportsToAdmins.value_, properties.emailReportsToAdmins)\n        setIfNil(\\.slurFilterRegex.value_, properties.slurFilterRegex)\n        setIfNil(\\.actorNameMaxLength.value_, properties.actorNameMaxLength)\n        setIfNil(\\.federationEnabled.value_, properties.federationEnabled)\n        setIfNil(\\.captchaEnabled.value_, properties.captchaEnabled)\n        setIfNil(\\.captchaDifficulty.value_, properties.captchaDifficulty)\n        setIfNil(\\.registrationMode.value_, properties.registrationMode)\n        setIfNil(\\.federationSignedFetch.value_, properties.federationSignedFetch)\n        setIfNil(\\.defaultPostListingMode.value_, properties.defaultPostListingMode)\n        setIfNil(\\.defaultPostSortType.value_, properties.defaultPostSortType)\n        setIfNil(\\.userCount.value_, properties.userCount)\n        setIfNil(\\.postCount.value_, properties.postCount)\n        setIfNil(\\.commentCount.value_, properties.commentCount)\n        setIfNil(\\.communityCount.value_, properties.communityCount)\n        setIfNil(\\.activeUserCount.value_, properties.activeUserCount)\n        setIfNil(\\.software.value_, properties.software)\n        setIfNil(\\.allowedLanguageIds.value_, properties.allowedLanguageIds)\n        setIfNil(\\.blockedUrls.value_, properties.blockedUrls)\n        setIfNil(\\.administrators.value_, properties.administrators)\n    }\n    \n    // MARK: Upgrades\n    \n    public func upgrade() async throws {\n        try await updateQueue.upgrade()\n    }\n    \n    /// Gets this instance using the ApiClient local to this instance\n    public func getLocal() async throws -> Instance {\n        if apiIsLocal { return self }\n        \n        let localApi = ApiClient.getApiClient(url: actorId.hostUrl, username: nil)\n        return try await localApi.getMyInstance()\n    }\n    \n    public func refresh() async throws {\n        try await updateQueue.refresh()\n    }\n    \n    public func fetchUpgraded() async throws -> InstanceProperties {\n        let externalApi: ApiClient = apiIsLocal ? api : .getApiClient(url: actorId.url, username: nil)\n        let snapshot = try await externalApi.repository.getMyInstance()\n        return await .init(api: api, snapshot: .instance3(snapshot))\n    }\n    \n    public func resolve(with api: ApiClient) async throws -> Instance {\n        guard let instance = try await api.getCommunityOfInstance(actorId: actorId).instance.value as? Instance else {\n            throw InstanceUpgradeError.noSiteReturned\n        }\n        return instance\n    }\n    \n}\n\n// MARK: Computed\n\npublic extension Instance {\n    @inlinable\n    var name: String { host }\n    \n    func language(withId id: Int) -> Locale.Language? {\n        guard let allLanguages = allLanguages.value else { return nil }\n        return allLanguages[safeIndex: id - 1]\n    }\n    \n    func getLanguageId(for language: Locale.Language) -> Int? {\n        guard let allLanguages = allLanguages.value else { return nil }\n        return allLanguages.firstIndex(of: language)?.advanced(by: 1)\n    }\n    \n    func languages(withIds ids: Set<Int>) -> [Locale.Language] {\n        ids.lazy.sorted(by: <).compactMap { self.language(withId: $0) }\n    }\n    \n    var allowedLanguages: Set<Locale.Language>? {\n        guard let allowedLanguageIds = allowedLanguageIds.value else { return nil }\n        return Set(allowedLanguageIds.lazy.compactMap { self.language(withId: $0) })\n    }\n    \n    var guestApi: ApiClient {\n        .getApiClient(url: local ? api.baseUrl : actorId.hostUrl, username: nil)\n    }\n}\n\n// MARK: Interactions\n\npublic extension Instance {\n    \n    // Add Admin\n    \n    func addAdmin(personId: Int, added: Bool) {\n        Task {\n            await updateQueue.addItem { properties in\n                let snapshots = try await self.api.repository.addAdmin(personId: personId, added: added)\n                let updatedAdministrators = await self.api.caches.person.getModels(api: self.api, from: snapshots.map { .person2($0) })\n                \n                // update person's admin status\n                // only need to do this manually if removing admin, otherwise handled by above caching logic\n                if !added, let person = self.api.caches.person.retrieveModel(cacheId: personId) {\n                    person.isAdmin.value_ = false\n                }\n                \n                var properties = properties\n                properties.administrators = updatedAdministrators\n                return properties\n            }\n        }\n    }\n    \n    // Username Validity\n    \n    var usernameIsValidForNewAccount: ((String) async throws -> UsernameValidity)? {\n        if let actorNameMaxLength = actorNameMaxLength.value {\n            return { try await self.usernameIsValidForNewAccount($0, actorNameMaxLength: actorNameMaxLength) }\n        }\n        return nil\n    }\n    \n    private func usernameIsValidForNewAccount(_ username: String, actorNameMaxLength: Int) async throws -> UsernameValidity {\n        guard username.count >= 3 else {\n            return .invalid(.tooShort(minLength: 3))\n        }\n        guard username.count <= actorNameMaxLength else {\n            return .invalid(.tooLong(maxLength: actorNameMaxLength))\n        }\n        \n        // Relevant backend code https://github.com/LemmyNet/lemmy/blob/5095092d3a6b0c194295e2cf3034d2b9abf8db54/crates/utils/src/utils/validation.rs#L94\n        \n        let regex = /^(?:[a-zA-Z0-9_]+|[0-9_\\p{Arabic}]+|[0-9_\\p{Cyrillic}]+)$/\n        \n        if try regex.wholeMatch(in: username) == nil {\n            // If username isn't english, give a generic error\n            let englishRegex = /[^\\p{Arabic}\\p{Cyrillic}]+/\n            if try englishRegex.wholeMatch(in: username) == nil { return .invalid(.other) }\n            \n            // If the username *is* in english, we can be more descriptive\n            let invalidCharacters = username.filter { char in\n                if char == \"_\" { return false }\n                guard let scalar = char.unicodeScalars.first, char.unicodeScalars.count == 1 else { return true }\n                if scalar.value >= 65, scalar.value <= 90 { return false } // Uppercase\n                if scalar.value >= 97, scalar.value <= 122 { return false } // Lowercase\n                if scalar.value >= 48, scalar.value <= 57 { return false } // Numbers\n                return true\n            }\n            \n            if !invalidCharacters.isEmpty {\n                return .invalid(.containsInvalidCharacters(Set(invalidCharacters)))\n            }\n            \n            assertionFailure()\n            return .invalid(.other)\n        }\n        \n        do {\n            _ = try await api.getPerson(username: username)\n            return .taken\n        } catch ApiClientError.noEntityFound {\n            return .available\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Instance/InstanceProperties.swift",
    "content": "//\n//  InstanceProperties.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-03-13.\n//\n\nimport Foundation\n\npublic struct InstanceProperties: UnifiedPropertiesProviding {\n    // From Instance1Snapshot, guaranteed to always be present\n    let actorId: ActorIdentifier\n    let id: Int\n    let instanceId: Int\n    let created: Date\n    let updated: Date?\n    let publicKey: String\n    var displayName: String\n    var description: String?\n    var shortDescription: String?\n    var avatar: URL?\n    var banner: URL?\n    var lastRefresh: Date\n    var contentWarning: String?\n    \n    // From Instance2Snapshot\n    var setup: Bool?\n    var voteFederationMode: VoteFederationMode?\n    var nsfwContentEnabled: Bool?\n    var communityCreationRestrictedToAdmins: Bool?\n    var emailVerificationRequired: Bool?\n    var applicationQuestion: String??\n    var isPrivate: Bool?\n    var defaultTheme: String?\n    var defaultFeed: ListingType?\n    var legalInformation: String??\n    var hideModlogNames: Bool?\n    var emailApplicationsToAdmins: Bool?\n    var emailReportsToAdmins: Bool?\n    var slurFilterRegex: String??\n    var actorNameMaxLength: Int?\n    var federationEnabled: Bool?\n    var captchaEnabled: Bool?\n    var captchaDifficulty: CaptchaDifficulty??\n    var registrationMode: RegistrationMode?\n    var federationSignedFetch: Bool??\n    var defaultPostListingMode: PostFeedViewMode??\n    var defaultPostSortType: PostSortType??\n    var userCount: Int?\n    var postCount: Int?\n    var commentCount: Int?\n    var communityCount: Int?\n    var activeUserCount: ActiveUserCount?\n    \n    // From Instance3Snapshot\n    let allLanguages: [Locale.Language]?\n    var software: SiteSoftware?\n    var allowedLanguageIds: Set<Int>?\n    var blockedUrls: [InstanceUrlBlockRecord]??\n    var administrators: [Person]?\n    \n    // Constructs an InstanceProperties from a given snapshot\n    @MainActor\n    public init(api: ApiClient, snapshot: AnyInstanceSnapshot) {\n        let snapshot1: Instance1Snapshot\n        let snapshot2: Instance2Snapshot?\n        let snapshot3: Instance3Snapshot?\n        switch snapshot {\n        case let .instance1(instance1Snapshot):\n            snapshot1 = instance1Snapshot\n            snapshot2 = nil\n            snapshot3 = nil\n        case let .instance2(instance2Snapshot):\n            snapshot1 = instance2Snapshot.instance\n            snapshot2 = instance2Snapshot\n            snapshot3 = nil\n        case let .instance3(instance3Snapshot):\n            snapshot1 = instance3Snapshot.instance.instance\n            snapshot2 = instance3Snapshot.instance\n            snapshot3 = instance3Snapshot\n        }\n        \n        if let snapshot3 {\n            allLanguages = snapshot3.allLanguages\n            software = snapshot3.software\n            allowedLanguageIds = snapshot3.allowedLanguageIds\n            blockedUrls = snapshot3.blockedUrls\n            administrators = api.caches.person.getModels(api: api, from: snapshot3.administrators.map { .person2($0) })\n        } else {\n            allLanguages = nil // needs special handling because it's a let\n        }\n        \n        if let snapshot2 {\n            setup = snapshot2.setup\n            voteFederationMode = snapshot2.voteFederationMode\n            nsfwContentEnabled = snapshot2.nsfwContentEnabled\n            communityCreationRestrictedToAdmins = snapshot2.communityCreationRestrictedToAdmins\n            emailVerificationRequired = snapshot2.emailVerificationRequired\n            applicationQuestion = snapshot2.applicationQuestion\n            isPrivate = snapshot2.isPrivate\n            defaultTheme = snapshot2.defaultTheme\n            defaultFeed = snapshot2.defaultFeed\n            legalInformation = snapshot2.legalInformation\n            hideModlogNames = snapshot2.hideModlogNames\n            emailApplicationsToAdmins = snapshot2.emailApplicationsToAdmins\n            emailReportsToAdmins = snapshot2.emailReportsToAdmins\n            slurFilterRegex = snapshot2.slurFilterRegex\n            actorNameMaxLength = snapshot2.actorNameMaxLength\n            federationEnabled = snapshot2.federationEnabled\n            captchaEnabled = snapshot2.captchaEnabled\n            captchaDifficulty = snapshot2.captchaDifficulty\n            registrationMode = snapshot2.registrationMode\n            federationSignedFetch = snapshot2.federationSignedFetch\n            defaultPostListingMode = snapshot2.defaultPostListingMode\n            defaultPostSortType = snapshot2.defaultPostSortType\n            userCount = snapshot2.userCount\n            postCount = snapshot2.postCount\n            commentCount = snapshot2.commentCount\n            communityCount = snapshot2.communityCount\n            activeUserCount = snapshot2.activeUserCount\n        }\n        \n        actorId = snapshot1.actorId\n        id = snapshot1.id\n        instanceId = snapshot1.instanceId\n        created = snapshot1.created\n        updated = snapshot1.updated\n        publicKey = snapshot1.publicKey\n        displayName = snapshot1.displayName\n        description = snapshot1.description\n        shortDescription = snapshot1.shortDescription\n        avatar = snapshot1.avatar\n        banner = snapshot1.banner\n        lastRefresh = snapshot1.lastRefresh\n        contentWarning = snapshot1.contentWarning\n    }\n    \n    public mutating func merge(_ other: InstanceProperties) {\n        // tier 1 properties: simple assignment\n        self.displayName = other.displayName\n        self.description = other.description\n        self.shortDescription = other.shortDescription\n        self.avatar = other.avatar\n        self.banner = other.banner\n        self.lastRefresh = other.lastRefresh\n        self.contentWarning = other.contentWarning\n        \n        // tier 2, 3 properties: only assign if incoming non-nil\n        self.setup = other.setup ?? self.setup\n        self.voteFederationMode = other.voteFederationMode ?? self.voteFederationMode\n        self.nsfwContentEnabled = other.nsfwContentEnabled ?? self.nsfwContentEnabled\n        self.communityCreationRestrictedToAdmins = other.communityCreationRestrictedToAdmins ?? self.communityCreationRestrictedToAdmins\n        self.emailVerificationRequired = other.emailVerificationRequired ?? self.emailVerificationRequired\n        self.applicationQuestion = other.applicationQuestion ?? self.applicationQuestion\n        self.isPrivate = other.isPrivate ?? self.isPrivate\n        self.defaultTheme = other.defaultTheme ?? self.defaultTheme\n        self.defaultFeed = other.defaultFeed ?? self.defaultFeed\n        self.legalInformation = other.legalInformation ?? self.legalInformation\n        self.hideModlogNames = other.hideModlogNames ?? self.hideModlogNames\n        self.emailApplicationsToAdmins = other.emailApplicationsToAdmins ?? self.emailApplicationsToAdmins\n        self.emailReportsToAdmins = other.emailReportsToAdmins ?? self.emailReportsToAdmins\n        self.slurFilterRegex = other.slurFilterRegex ?? self.slurFilterRegex\n        self.actorNameMaxLength = other.actorNameMaxLength ?? self.actorNameMaxLength\n        self.federationEnabled = other.federationEnabled ?? self.federationEnabled\n        self.captchaEnabled = other.captchaEnabled ?? self.captchaEnabled\n        self.captchaDifficulty = other.captchaDifficulty ?? self.captchaDifficulty\n        self.registrationMode = other.registrationMode ?? self.registrationMode\n        self.federationSignedFetch = other.federationSignedFetch ?? self.federationSignedFetch\n        self.defaultPostListingMode = other.defaultPostListingMode ?? self.defaultPostListingMode\n        self.defaultPostSortType = other.defaultPostSortType ?? self.defaultPostSortType\n        self.userCount = other.userCount ?? self.userCount\n        self.postCount = other.postCount ?? self.postCount\n        self.commentCount = other.commentCount ?? self.commentCount\n        self.communityCount = other.communityCount ?? self.communityCount\n        self.activeUserCount = other.activeUserCount ?? self.activeUserCount\n        \n        self.software = other.software ?? self.software\n        self.allowedLanguageIds = other.allowedLanguageIds ?? self.allowedLanguageIds\n        self.blockedUrls = other.blockedUrls ?? self.blockedUrls\n        self.administrators = other.administrators ?? self.administrators\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Instance/InstanceStub.swift",
    "content": "//\n//  File.swift\n//\n//\n//  Created by Sjmarf on 28/05/2024.\n//\n\nimport Foundation\n\npublic enum InstanceUpgradeError: Error {\n    case noPostReturned\n    case noCommunityReturned\n    case noSiteReturned\n}\n\npublic struct InstanceStub: Hashable {\n    public var api: ApiClient\n    public let actorId: ActorIdentifier\n    \n    public var local: Bool { actorId.url == api.baseUrl }\n    \n    public init(api: ApiClient, actorId: ActorIdentifier) {\n        self.api = api\n        self.actorId = actorId\n    }\n    \n    public func asLocal() -> Self {\n        .init(api: .getApiClient(url: actorId.hostUrl, username: nil), actorId: actorId)\n    }\n    \n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(actorId)\n    }\n    \n    public static func == (lhs: InstanceStub, rhs: InstanceStub) -> Bool {\n        lhs.actorId == rhs.actorId\n    }\n    \n    /// Gets the instance this stub refers to using that instance's local API\n    public func getLocalInstance() async throws -> Instance {\n        return try await self.asLocal().api.getMyInstance()\n    }\n    \n    /// Gets the instance this stub refers to using the stub's current API\n    public func getInstance() async throws -> Instance {\n        let community = try await api.getCommunityOfInstance(actorId: actorId)\n        let instance = try await community.fetchUpgraded().instance\n  \n        guard let instance = instance as? Instance else {\n            throw InstanceUpgradeError.noSiteReturned\n        }\n        return instance\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/InstanceBanType.swift",
    "content": "//\n//  BanType.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/02/2024.\n//\n\nimport Foundation\n\npublic enum InstanceBanType: Equatable {\n    case notBanned\n    case permanentlyBanned\n    case temporarilyBanned(expires: Date)\n    \n    var expiryDate: Date? {\n        switch self {\n        case let .temporarilyBanned(expires): expires\n        default: nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/InstanceUrlBlockRecord.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic struct InstanceUrlBlockRecord: Hashable {\n    let id: Int\n    let created: Date\n    let updated: Date?\n    let url: URL\n    \n    public init(from blocklist: LemmyLocalSiteUrlBlocklist) throws(ApiClientError) {\n        self.id = blocklist.id\n        \n        if let published = blocklist.publishedAt ?? blocklist.published {\n            self.created = published\n        } else {\n            throw .responseMissingRequiredData(\"LemmyLocalSiteUrlBlocklist published\")\n        }\n        \n        self.updated = blocklist.updatedAt ?? blocklist.updated\n        \n        guard let url = URL(string: blocklist.url) else {\n            throw .responseMissingRequiredData(\"LemmyLocalSiteUrlBlocklist Invalid URL\")\n        }\n        self.url = url\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Interactable/InteractableProviding.swift",
    "content": "//\n//  InteractableProviding.swift\n//  Mlem\n//\n//  Created by Sjmarf on 30/03/2024.\n//\n\nimport Foundation\n\n/// Represents a post/comment that you *should* be able to interact with, but you cannot actually interact with due to the model being too low-tier.\npublic protocol InteractableProviding:\n    AnyObject,\n    ContentModel,\n    ReportableProviding,\n    ContentIdentifiable,\n    RemovableProviding {\n    var created: Date { get }\n    var updated: Date? { get }\n    \n    var votes: ExpectedValue<VotesModel> { get }\n    var saved: ExpectedValue<Bool> { get }\n    var commentCount: ExpectedValue<Int> { get }\n    var creator: ExpectedValue<Person> { get }\n    var community: ExpectedValue<Community> { get }\n    var creatorIsAdmin: ExpectedValue<Bool> { get }\n    var creatorIsModerator: ExpectedValue<Bool> { get }\n    \n    var updateVote: ((ScoringOperation) -> Void)? { get }\n    func updateSaved(_ newValue: Bool)\n    func reply(content: String, languageId: Int?) async throws -> Comment\n    \n    var downvotesEnabled: Bool { get }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Internal/LemmyURL.swift",
    "content": "//\n//  LemmyURL.swift\n//  Mlem\n//\n//  Created by mormaer on 15/09/2023.\n//\n//\n\nimport Foundation\n\nstruct LemmyURL {\n    let url: URL\n    \n    init?(string: String?) {\n        guard let string else {\n            return nil\n        }\n        \n        if let url = URL(string: string) {\n            self.url = url\n        } else if let encoded = string.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed), let url = URL(string: encoded) {\n            self.url = url\n        } else {\n            return nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Internal/ScoringOperation.swift",
    "content": "//\n//  ScoringOperation.swift\n//  Mlem\n//\n//  Created by mormaer on 16/08/2023.\n//\n//\n\nimport Foundation\nimport SwiftUI\n\npublic enum ScoringOperation: Int, Decodable, CustomStringConvertible {\n    case upvote = 1\n    case downvote = -1\n    case none = 0\n\n    public var upvoteValue: Int { self == .upvote ? 1 : 0 }\n    public var downvoteValue: Int { self == .downvote ? 1 : 0 }\n\n    public var description: String {\n        switch self {\n        case .upvote:\n            \"Upvote\"\n        case .downvote:\n            \"Downvote\"\n        case .none:\n            \"No Vote\"\n        }\n    }\n\n    var booleanValue: Bool? {\n        switch self {\n        case .upvote: true\n        case .downvote: false\n        case .none: nil\n        }\n    }\n    \n    init(_ bool: Bool?) {\n        self = switch bool {\n        case true: .upvote\n        case false: .downvote\n        case nil: .none\n        }\n    }\n}\n\npublic extension ScoringOperation {\n    /// Non-optional initializer; if int is nil or invalid, returns .none\n    static func guaranteedInit(from int: Int?) -> ScoringOperation {\n        guard let int else {\n            return .none\n        }\n        \n        if let value = ScoringOperation(rawValue: int) {\n            return value\n        } else {\n            return .none\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Internal/SiteSoftware/SiteSoftware.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic struct SiteSoftware: Codable, Hashable {\n    public let type: SiteSoftwareType\n    public let version: SiteVersion\n    \n    public init(type: SiteSoftwareType, version: SiteVersion) {\n        self.type = type\n        self.version = version\n    }\n    \n    public func supports(_ feature: Feature) -> Bool {\n        switch type {\n        case .lemmy: LemmyConnection.supports(feature, version: version)\n        case .pieFed: PieFedConnection.supports(feature, version: version)\n        }\n    }\n}\n\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Internal/SiteSoftware/SiteSoftwareType.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic enum SiteSoftwareType: String, Codable, Sendable, CaseIterable {\n    case lemmy, pieFed\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Internal/SiteVersion/SiteVersion+EndpointVersion.swift",
    "content": "//\n//  SiteVersion+EndpointVersion.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-21.\n//\n\nimport Foundation\n\npublic enum LemmyEndpointVersion: Hashable, Sendable {\n    case v3, v4\n    \n    var pathComponent: String {\n        switch self {\n        case .v3: \"v3\"\n        case .v4: \"v4\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Internal/SiteVersion/SiteVersion.swift",
    "content": "//\n//  ApiSiteVersionNumber.swift\n//  Mlem\n//\n//  Created by Sjmarf on 09/09/2023.\n//\nimport Foundation\n\npublic enum SiteVersion: Equatable, Hashable {\n    case release(major: Int, minor: Int, patch: Int)\n    case other(String)\n    case zero\n    case infinity\n    \n    public init(_ version: String) {\n        let parts = version.split(separator: \"-\")\n        if let firstPart = parts.first {\n            let components = firstPart.split(separator: \".\").compactMap { Int($0) }\n            if components.count == 3 {\n                self = .release(major: components[0], minor: components[1], patch: components[2])\n            } else {\n                self = .other(version)\n            }\n        } else {\n            self = .other(version)\n        }\n    }\n    \n    // swiftlint: disable large_tuple\n    public var parts: (Int, Int, Int)? {\n        switch self {\n        case let .release(major, minor, patch):\n            return (major, minor, patch)\n        default:\n            return nil\n        }\n    }\n\n    // swiftlint: enable large_tuple\n}\n\nextension SiteVersion: CustomStringConvertible {\n    public var description: String {\n        switch self {\n        case .zero:\n            return \"zero\"\n        case .infinity:\n            return \"infinity\"\n        case let .release(major, minor, patch):\n            return \"\\(major).\\(minor).\\(patch)\"\n        case let .other(string):\n            return string\n        }\n    }\n}\n\nextension SiteVersion: Codable {\n    public init(from decoder: Decoder) throws {\n        let container = try decoder.singleValueContainer()\n        let versionString = try container.decode(String.self)\n        self.init(versionString)\n    }\n    \n    public func encode(to encoder: Encoder) throws {\n        var container = encoder.singleValueContainer()\n        try container.encode(String(describing: self))\n    }\n}\n\nextension SiteVersion: Comparable {\n    public static func < (lhs: SiteVersion, rhs: SiteVersion) -> Bool {\n        switch (lhs, rhs) {\n        case (.release, .release):\n            return lhs.parts! < rhs.parts!\n            \n        case (.zero, _), (_, .infinity):\n            return true\n            \n        case (_, .zero), (.infinity, _):\n            return false\n        default:\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/BlockListSnapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension BlockListSnapshot {\n    init(from myUserInfo: LemmyMyUserInfo) throws(ApiClientError) {\n        self.people = myUserInfo.personBlocks.reduce(into: [:]) {\n            if let actorId = $1.person.apId ?? $1.person.actorId {\n                $0[actorId] = $1.person.id\n            }\n        }\n        \n        self.communities = myUserInfo.communityBlocks.reduce(into: [:]) {\n            if let actorId = $1.community.apId ?? $1.community.actorId {\n                $0[actorId] = $1.community.id\n            }\n        }\n        \n        if let instanceCommunitiesBlocks = myUserInfo.instanceCommunitiesBlocks {\n            self.instances = instanceCommunitiesBlocks.reduce(into: [:]) {\n                let actorId: ActorIdentifier = .instance(host: $1.domain)\n                $0[actorId] = $1.id\n            }\n        } else if let instanceBlocks = myUserInfo.instanceBlocks {\n            self.instances = instanceBlocks.reduce(into: [:]) {\n                let actorId: ActorIdentifier = .instance(host: $1.instance.domain)\n                $0[actorId] = $1.instance.id\n            }\n        } else {\n            throw .responseMissingRequiredData(\"LemmyMyUserInfo instanceBlocks (BlockListSnapshot)\")\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Comment1Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Comment1Snapshot {\n    init(from comment: LemmyComment) throws(ApiClientError) {\n        let parentCommentIds = comment.path\n            .split(separator: \".\")\n            .dropFirst()\n            .dropLast()\n            .compactMap { Int($0) }\n        \n        guard let published = comment.publishedAt ?? comment.published else {\n            throw .responseMissingRequiredData(\"LemmyComment published\")\n        }\n        \n        self.init(\n            actorId: comment.apId,\n            id: comment.id,\n            creatorId: comment.creatorId,\n            postId: comment.postId,\n            parentCommentIds: parentCommentIds,\n            created: published,\n            content: comment.content,\n            updated: comment.updatedAt ?? comment.updated,\n            distinguished: comment.distinguished,\n            languageId: comment.languageId,\n            deleted: comment.deleted,\n            removed: comment.removed\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Comment2Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Comment2Snapshot {\n    init(from comment: LemmyCommentView) throws(ApiClientError) {\n        guard let commentCount = comment.comment.childCount ?? comment.counts?.childCount else {\n            throw .responseMissingRequiredData(\"LemmyCommentView childCount\")\n        }\n\n        let saved: Bool\n        if let saved_ = comment.saved {\n            saved = saved_\n        } else {\n            saved = comment.commentActions?.savedAt != nil\n        }\n        \n        let votes: VotesModel\n        if let counts = comment.counts {\n            votes = .init(from: counts, myVote: .guaranteedInit(from: comment.myVote))\n        } else if let upvotes = comment.comment.upvotes, let downvotes = comment.comment.downvotes {\n            votes = .init(\n                upvotes: upvotes,\n                downvotes: downvotes,\n                myVote: .init(comment.commentActions?.voteIsUpvote)\n            )\n        } else {\n            throw .responseMissingRequiredData(\"LemmyCommentView score\")\n        }\n\n        try self.init(\n            comment: .init(from: comment.comment),\n            creator: .init(from: comment.creator),\n            post: .init(from: comment.post),\n            community: .init(from: comment.community),\n            commentCount: commentCount,\n            creatorIsModerator: comment.creatorIsModerator,\n            creatorIsAdmin: comment.creatorIsAdmin,\n            creatorBannedFromCommunity: comment.creatorBannedFromCommunity,\n            votes: votes,\n            saved: saved\n        )\n    }\n    \n    init(from report: LemmyCommentReportView) throws(ApiClientError) {\n        guard let commentCount = report.comment.childCount ?? report.counts?.childCount else {\n            throw .responseMissingRequiredData(\"LemmyCommentReportView childCount\")\n        }\n\n        let saved: Bool\n        if let saved_ = report.saved {\n            saved = saved_\n        } else {\n            saved = report.commentActions?.savedAt != nil\n        }\n        \n        let votes: VotesModel\n        if let counts = report.counts {\n            votes = .init(from: counts, myVote: .guaranteedInit(from: report.myVote))\n        } else if let upvotes = report.comment.upvotes, let downvotes = report.comment.downvotes {\n            votes = .init(\n                upvotes: upvotes,\n                downvotes: downvotes,\n                myVote: .init(report.commentActions?.voteIsUpvote)\n            )\n        } else {\n            throw .responseMissingRequiredData(\"LemmyCommentReportView score\")\n        }\n\n        try self.init(\n            comment: .init(from: report.comment),\n            creator: .init(from: report.commentCreator),\n            post: .init(from: report.post),\n            community: .init(from: report.community),\n            commentCount: commentCount,\n            creatorIsModerator: report.creatorIsModerator ?? false,\n            creatorIsAdmin: report.creatorIsAdmin ?? false,\n            creatorBannedFromCommunity: report.creatorBannedFromCommunity,\n            votes: votes,\n            saved: saved\n        )\n    }\n\n    init(from reply: LemmyCommentReplyView) throws(ApiClientError) {\n        try self.init(\n            comment: .init(from: reply.comment),\n            creator: .init(from: reply.creator),\n            post: .init(from: reply.post),\n            community: .init(from: reply.community),\n            commentCount: reply.comment.childCount ?? reply.counts.childCount,\n            creatorIsModerator: reply.creatorIsModerator,\n            creatorIsAdmin: reply.creatorIsAdmin,\n            creatorBannedFromCommunity: reply.creatorBannedFromCommunity,\n            votes: .init(from: reply.counts, myVote: .guaranteedInit(from: reply.myVote)),\n            saved: reply.saved\n        )\n    }\n\n    init(from mention: LemmyPersonCommentMentionView) throws(ApiClientError) {\n        try self.init(\n            comment: .init(from: mention.comment),\n            creator: .init(from: mention.creator),\n            post: .init(from: mention.post),\n            community: .init(from: mention.community),\n            commentCount: mention.comment.childCount ?? mention.counts.childCount,\n            creatorIsModerator: mention.creatorIsModerator,\n            creatorIsAdmin: mention.creatorIsAdmin,\n            creatorBannedFromCommunity: mention.creatorBannedFromCommunity,\n            votes: .init(from: mention.counts, myVote: .guaranteedInit(from: mention.myVote)),\n            saved: mention.saved\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/CommentSortType+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\n// This is reluncantly public because Mlem uses it; this should really be `internal`\npublic extension CommentSortType {\n    init(_ apiSortType: LemmyCommentSortType) {\n        self = switch apiSortType {\n        case .hot: .hot\n        case .top: .top(.allTime)\n        case .new: .new\n        case .old: .old\n        case .controversial: .controversial\n        }\n    }\n    \n    internal func apiType(for endpoint: LemmyEndpointVersion) throws(ApiClientError) -> LemmySearchSortTypeBridge {\n        switch endpoint {\n        case .v3: .old(v3PostApiType)\n        case .v4: try .newOrUnsupported(v4SearchApiType)\n        }\n    }\n    \n    var v3CommentApiType: LemmyCommentSortType {\n        switch self {\n        case .new: .new\n        case .old: .old\n        case .hot: .hot\n        case .controversial: .controversial\n        case .top: .top\n        }\n    }\n    \n    var v3PostApiType: LemmySortType {\n        switch self {\n        case .new: .new\n        case .old: .old\n        case .hot: .hot\n        case .controversial: .controversial\n        case .top: .topAll\n        }\n    }\n    \n    var v4SearchApiType: LemmySearchSortType? {\n        switch self {\n        case .new: .new\n        case .old: .old\n        case .top: .top\n        default: nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Community1Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Community1Snapshot {\n    init(from community: LemmyCommunity) throws(ApiClientError) {\n        guard let actorId = community.apId ?? community.actorId else {\n            throw .responseMissingRequiredData(\"LemmyCommunity actorId\")\n        }\n        \n        guard let published = community.publishedAt ?? community.published else {\n            throw .responseMissingRequiredData(\"LemmyCommunity published\")\n        }\n\n        let description: String?\n        if let sidebar = community.sidebar {\n            description = sidebar\n        } else {\n            description = community.description\n        }\n        \n        self.init(\n            actorId: actorId,\n            id: community.id,\n            name: community.name,\n            created: published,\n            instanceId: community.instanceId,\n            updated: community.updatedAt ?? community.updated,\n            displayName: community.title,\n            description: description,\n            deleted: community.deleted,\n            removed: community.removed,\n            nsfw: community.nsfw,\n            avatar: community.icon,\n            banner: community.banner,\n            hidden: community.hidden ?? false, // TODO: 0.20 we shouldn't be null coalescing here\n            onlyModeratorsCanPost: community.postingRestrictedToMods,\n            allPropertiesPresent: true\n        )\n     }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Community2Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Community2Snapshot {\n    init(from community: LemmyCommunityView) throws(ApiClientError) {\n        guard let totalSubscribers = community.community.subscribers ?? community.counts?.subscribers,\n              let localSubscribers = community.community.subscribersLocal ?? community.counts?.subscribersLocal\n        else {\n            throw .responseMissingRequiredData(\"LemmyCommunityView subscriber count\")\n        }\n\n        let subscribed: Bool\n        if let subscribed_ = community.subscribed?.isSubscribed {\n            subscribed = subscribed_\n        } else {\n            subscribed = community.communityActions?.followState?.isSubscribed ?? false\n        }\n\n        let subscription = SubscriptionModel(\n            total: totalSubscribers,\n            local: localSubscribers,\n            subscribed: subscribed,\n            pending: community.communityActions?.followState == .pending || community.subscribed == .pending\n        )\n        \n        guard let postCount = community.counts?.posts ?? community.community.posts else {\n            throw .responseMissingRequiredData(\"LemmyCommunityView postCount\")\n        }\n        \n        guard let commentCount = community.counts?.comments ?? community.community.comments else {\n            throw .responseMissingRequiredData(\"LemmyCommunityView commentCount\")\n        }\n        \n        guard let activeUsers6Months = community.counts?.usersActiveHalfYear ?? community.community.usersActiveHalfYear,\n              let activeUsersMonth = community.counts?.usersActiveMonth ?? community.community.usersActiveMonth,\n              let activeUsersWeek = community.counts?.usersActiveWeek ?? community.community.usersActiveWeek,\n              let activeUsersDay = community.counts?.usersActiveDay ?? community.community.usersActiveDay else {\n            throw .responseMissingRequiredData(\"LemmyCommunityView activeUserCount\")\n        }\n\n        let activeUserCount = ActiveUserCount(\n            sixMonths: activeUsers6Months,\n            month: activeUsersMonth,\n            week: activeUsersWeek,\n            day: activeUsersDay\n        )\n        \n        let bannedFromCommunity: Bool?\n        if let actions = community.communityActions {\n            bannedFromCommunity = actions.banExpiresAt != nil\n        } else {\n            bannedFromCommunity = community.bannedFromCommunity\n        }\n\n        try self.init(\n            community: .init(from: community.community),\n            subscription: subscription,\n            postCount: postCount,\n            commentCount: commentCount,\n            activeUserCount: activeUserCount,\n            bannedFromCommunity: bannedFromCommunity\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Community3Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Community3Snapshot {\n    init(from community: LemmyGetCommunityResponse) throws(ApiClientError) {\n        let instance: Instance1Snapshot?\n        if let site = community.site {\n            instance = try .init(from: site)\n        } else {\n            instance = nil\n        }\n        \n        var moderators = [Person1Snapshot]()\n        for moderator in community.moderators {\n            try moderators.append(.init(from: moderator.moderator))\n        }\n\n        try self.init(\n            community: .init(from: community.communityView),\n            instance: instance,\n            moderators: moderators,\n            discussionLanguageIds: .init(community.discussionLanguages)\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/FederationMode+Lemmy.swift",
    "content": "//\n//  FederationMode+Lemmy.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-11-30.\n//\n\nimport Foundation\n\nextension FederationMode {\n    init(from federationMode: LemmyFederationMode) {\n        self = switch federationMode {\n        case .all: .all\n        case .local: .local\n        case .disable: .disable\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/ImageUpload1Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension ImageUpload1Snapshot {\n    init(from file: LemmyPictrsFile, baseUrl: URL) {\n        self.init(\n            url: baseUrl.appending(path: \"pictrs/image/\\(file.file)\"),\n            alias: file.file,\n            deleteToken: file.deleteToken\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/InboxNotificationSnapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension InboxNotificationSnapshot {\n    init(from replyView: LemmyCommentReplyView) throws(ApiClientError) {\n        try self.init(\n            id: LegacyNotificationIdWrapper(type: .reply, id: replyView.commentReply.id).hashValue,\n            contentId: replyView.commentReply.id,\n            read: replyView.commentReply.read,\n            content: .reply(.init(from: replyView))\n        )\n    }\n\n    init(from mentionView: LemmyPersonCommentMentionView) throws(ApiClientError) {\n        try self.init(\n            id: LegacyNotificationIdWrapper(type: .mention, id: mentionView.personMention.id).hashValue,\n            contentId: mentionView.personMention.id,\n            read: mentionView.personMention.read,\n            content: .mention(.init(from: mentionView))\n        )\n    }\n\n    init(from messageView: LemmyPrivateMessageView) throws(ApiClientError) {\n        guard let read = messageView.privateMessage.read else {\n            throw .responseMissingRequiredData(\"LemmyPrivateMessage read\")\n        }\n\n        try self.init(\n            id: LegacyNotificationIdWrapper(type: .message, id: messageView.privateMessage.id).hashValue,\n            contentId: messageView.privateMessage.id,\n            read: read,\n            content: .message(.init(from: messageView))\n        )\n    }\n\n    init(from notification: LemmyNotificationView) throws(ApiClientError) {\n        let contentId: Int\n        let content: InboxNotificationContentSnapshot\n\n        switch notification.data {\n        case let .privateMessage(message):\n            contentId = message.privateMessage.id\n            content = try .message(.init(from: message))\n        case let .comment(comment) where notification.notification.kind == .mention:\n            contentId = comment.comment.id\n            content = try .mention(.init(from: comment))\n        case let .comment(comment) where  notification.notification.kind == .reply:\n            contentId = comment.comment.id\n            content = try .reply(.init(from: comment))\n        default:\n            throw ApiClientError.featureUnsupported\n        }\n\n        self.init(\n            id: notification.notification.id,\n            contentId: contentId,\n            read: notification.notification.read,\n            content: content\n        )\n    }\n}\n\n// This can be removed once we drop support for < Lemmy 1.0\nprivate struct LegacyNotificationIdWrapper: Hashable {\n    let type: InboxNotificationContentType\n    let id: Int\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Instance1Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Instance1Snapshot {\n    init(from site: LemmySite) throws(ApiClientError) {\n        guard let actorId = site.apId ?? site.actorId else {\n            throw .responseMissingRequiredData(\"LemmySite actorId\")\n        }\n        \n        guard let published = site.publishedAt ?? site.published else {\n            throw .responseMissingRequiredData(\"LemmySite published\")\n        }\n\n        self.init(\n            actorId: actorId,\n            id: site.id,\n            instanceId: site.instanceId,\n            created: published,\n            updated: site.updatedAt ?? site.updated,\n            publicKey: site.publicKey ?? \"\",\n            displayName: site.name,\n            description: site.sidebar,\n            shortDescription: site.description,\n            avatar: site.icon,\n            banner: site.banner,\n            lastRefresh: site.lastRefreshedAt,\n            contentWarning: site.contentWarning\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Instance2Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Instance2Snapshot {\n    init(from site: LemmySiteView) throws(ApiClientError) {\n        let nsfwContentEnabled: Bool\n        if let blockNsfw = site.localSite.nsfwContentDisallowed {\n            nsfwContentEnabled = !blockNsfw\n        } else if let enableNsfw = site.localSite.enableNsfw {\n            nsfwContentEnabled = enableNsfw\n        } else {\n            throw .responseMissingRequiredData(\"ApiSiteView enableNsfw\")\n        }\n        \n        let userCount: Int\n        let postCount: Int\n        let commentCount: Int\n        let communityCount: Int\n        let activeUserCount: ActiveUserCount\n\n        if let counts = site.counts {\n            userCount = counts.users\n            postCount = counts.posts\n            commentCount = counts.comments\n            communityCount = counts.communities\n            activeUserCount = .init(\n                sixMonths: counts.usersActiveHalfYear,\n                month: counts.usersActiveMonth,\n                week: counts.usersActiveWeek,\n                day: counts.usersActiveDay\n            )\n        } else {\n            guard let users = site.localSite.users else { throw .responseMissingRequiredData(\"LemmySiteView users\") }\n            userCount = users\n            guard let posts = site.localSite.posts else { throw .responseMissingRequiredData(\"LemmySiteView posts\") }\n            postCount = posts\n            guard let comments = site.localSite.comments else { throw .responseMissingRequiredData(\"LemmySiteView comments\") }\n            commentCount = comments\n            guard let communities = site.localSite.communities else { throw .responseMissingRequiredData(\"LemmySiteView communities\") }\n            communityCount = communities\n            guard let sixMonths = site.localSite.usersActiveHalfYear else {\n                throw .responseMissingRequiredData(\"LemmySiteView active users\")\n            }\n            guard let month = site.localSite.usersActiveMonth else {\n                throw .responseMissingRequiredData(\"LemmySiteView active users\")\n            }\n            guard let week = site.localSite.usersActiveWeek else {\n                throw .responseMissingRequiredData(\"LemmySiteView active users\")\n            }\n            guard let day = site.localSite.usersActiveDay else {\n                throw .responseMissingRequiredData(\"LemmySiteView active users\")\n            }\n            activeUserCount = .init(\n                sixMonths: sixMonths,\n                month: month,\n                week: week,\n                day: day\n            )\n        }\n\n        let voteFederationMode: VoteFederationMode\n        if let commentDownvotes = site.localSite.commentDownvotes,\n            let commentUpvotes = site.localSite.commentUpvotes,\n            let postDownvotes = site.localSite.postDownvotes,\n            let postUpvotes = site.localSite.postUpvotes\n        {\n            voteFederationMode = .init(\n                postUpvote: .init(from: postUpvotes),\n                postDownvote: .init(from: postDownvotes),\n                commentUpvote: .init(from: commentUpvotes),\n                commentDownvote: .init(from: commentDownvotes)\n            )\n        } else if let enableDownvotes = site.localSite.enableDownvotes {\n            voteFederationMode = enableDownvotes ? .all : .downvotesDisabled\n        } else {\n            throw .responseMissingRequiredData(\"LemmySiteView downvoteFederationMode\")\n        }\n\n        try self.init(\n            instance: .init(from: site.site),\n            setup: site.localSite.siteSetup,\n            voteFederationMode: voteFederationMode,\n            nsfwContentEnabled: nsfwContentEnabled,\n            communityCreationRestrictedToAdmins: site.localSite.communityCreationAdminOnly,\n            emailVerificationRequired: site.localSite.requireEmailVerification ?? true,\n            applicationQuestion: site.localSite.applicationQuestion,\n            isPrivate: site.localSite.privateInstance,\n            defaultTheme: site.localSite.defaultTheme,\n            defaultFeed: .init(from: site.localSite.defaultPostListingType),\n            legalInformation: site.localSite.legalInformation,\n            hideModlogNames: site.localSite.hideModlogModNames ?? true, // Always hidden in Lemmy 1.0\n            emailApplicationsToAdmins: site.localSite.applicationEmailAdmins,\n            emailReportsToAdmins: site.localSite.reportsEmailAdmins,\n            slurFilterRegex: site.localSite.slurFilterRegex,\n            actorNameMaxLength: site.localSite.actorNameMaxLength ?? 20,\n            federationEnabled: site.localSite.federationEnabled,\n            captchaEnabled: site.localSite.captchaEnabled ?? false,\n            captchaDifficulty: site.localSite.captchaDifficulty.map(CaptchaDifficulty.init) ?? .none,\n            registrationMode: .init(from: site.localSite.registrationMode),\n            federationSignedFetch: site.localSite.federationSignedFetch,\n            defaultPostListingMode: site.localSite.defaultPostListingMode.map { .init(from: $0) },\n            defaultPostSortType: site.localSite.defaultSortType.map { .init($0) },\n            userCount: userCount,\n            postCount: postCount,\n            commentCount: commentCount,\n            communityCount: communityCount,\n            activeUserCount: activeUserCount\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Instance3Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Instance3Snapshot {\n    init(from site: LemmyGetSiteResponse) throws(ApiClientError) {\n        let blockedUrls: [InstanceUrlBlockRecord]?\n        if let blockedUrls_ = site.blockedUrls {\n            var newBlockedUrls: [InstanceUrlBlockRecord] = []\n            newBlockedUrls.reserveCapacity(blockedUrls_.count)\n            for url in blockedUrls_ {\n                try newBlockedUrls.append(.init(from: url))\n            }\n            blockedUrls = newBlockedUrls\n        } else {\n            blockedUrls = nil\n        }\n    \n        var administrators: [Person2Snapshot] = []\n        administrators.reserveCapacity(site.admins.count)\n        for admin in site.admins {\n            try administrators.append(.init(from: admin))\n        }\n\n        try self.init(\n            instance: .init(from: site.siteView),\n            allLanguages: site.allLanguages.compactMap { .init($0) },\n            software: .init(type: .lemmy, version: .init(site.version)),\n            allowedLanguageIds: Set(site.discussionLanguages).subtracting([0]),\n            blockedUrls: blockedUrls,\n            administrators: administrators\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/LegacySortTimeRangeLimit+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//  \n\nimport Foundation\n\ninternal extension LegacySortTimeRangeLimit {\n    init?(_ legacyApiSortType: LemmySortType) {\n        if let value: Self = switch legacyApiSortType {\n        case .topHour: .hour\n        case .topSixHour: .sixHour\n        case .topTwelveHour: .twelveHour\n        case .topDay: .day\n        case .topWeek: .week\n        case .topMonth: .month\n        case .topThreeMonths: .threeMonth\n        case .topSixMonths: .sixMonth\n        case .topNineMonths: .nineMonth\n        case .topYear: .year\n        default: nil\n        } {\n            self = value\n        } else {\n            return nil\n        }\n    }\n    \n    var legacyApiSortType: LemmySortType {\n        switch self {\n        case .hour: .topHour\n        case .sixHour: .topSixHour\n        case .twelveHour: .topTwelveHour\n        case .day: .topDay\n        case .week: .topWeek\n        case .month: .topMonth\n        case .threeMonth: .topThreeMonths\n        case .sixMonth: .topSixMonths\n        case .nineMonth: .topNineMonths\n        case .year: .topYear\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/LemmyPersonSavedCombinedView+Extensions.swift",
    "content": "//\n//  LemmyPersonSavedCombinedView+Extensions.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-11-25.\n//\n\nimport Foundation\n\nextension LemmyPostCommentCombinedView {\n    var postValue: LemmyPostView? {\n        switch self {\n        case let .post(post): post\n        case .comment: nil\n        }\n    }\n\n    var commentValue: LemmyCommentView? {\n        switch self {\n        case let .comment(comment): comment\n        case .post: nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/ListingType+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension ListingType {\n    init(from type: LemmyListingType) throws(ApiClientError) {\n        let value: Self? = switch type {\n        case .all: .all\n        case .local: .local\n        case .subscribed: .subscribed\n        case .moderatorView: .moderated\n        case .suggested: .suggested\n        }\n        \n        guard let value else {\n            throw .featureUnsupported\n        }\n        \n        self = value\n    }\n    \n    var apiType: LemmyListingType? {\n        switch self {\n        case .all: .all\n        case .local: .local\n        case .subscribed: .subscribed\n        case .moderated: .moderatorView\n        case .popular: nil\n        case .suggested: .suggested\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Locale.Language+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Locale.Language {\n    init?(_ apiLanguage: LemmyLanguage) {\n        if apiLanguage.code == \"und\" {\n            return nil\n        } else {\n            self = .init(identifier: apiLanguage.code)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Message1Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Message1Snapshot {\n    init(from message: LemmyPrivateMessage) throws(ApiClientError) {\n        guard let published = message.publishedAt ?? message.published else {\n            throw .responseMissingRequiredData(\"LemmyPrivateMessage published\")\n        }\n\n        self.init(\n            actorId: message.apId,\n            id: message.id,\n            creatorId: message.creatorId,\n            recipientId: message.recipientId,\n            created: published,\n            content: message.content,\n            updated: message.updatedAt ?? message.updated,\n            read: message.read ?? false, // Temporary: Fix in 1.0\n            deleted: message.deleted\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Message2Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Message2Snapshot {\n    init(from message: LemmyPrivateMessageView) throws(ApiClientError) {\n        try self.init(\n            message: .init(from: message.privateMessage),\n            creator: .init(from: message.creator),\n            recipient: .init(from: message.recipient)\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/ModlogEntryContentSnapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-09-28.\n//\n\nimport Foundation\n\n// MARK: Lemmy 1.0\n\nextension ModlogEntryContentSnapshot {\n    init?(from view: LemmyModlogView) throws(ApiClientError) {\n        let value: Self? = switch view.modlog.kind {\n        case .modRemovePost:\n            // Temporarily disabled, see #2558\n            // try Self.modRemovePost(view: view)\n            nil\n        case .modLockPost:\n            try Self.modLockPost(view: view)\n        case .modRemoveComment:\n            // Temporarily disabled, see #2558\n            // try Self.modRemoveComment(view: view)\n            nil\n        case .modBanFromCommunity:\n            try Self.modBanFromCommunity(view: view)\n        case .modTransferCommunity:\n            try Self.modTransferCommunity(view: view)\n        case .adminPurgePerson:\n            try Self.adminPurgePerson(view: view)\n        case .adminPurgeCommunity:\n            try Self.adminPurgeCommunity(view: view)\n        case .adminPurgePost:\n            try Self.adminPurgePost(view: view)\n        case .adminPurgeComment:\n            try Self.adminPurgeComment(view: view)\n        case .adminAdd:\n            try Self.adminAdd(view: view)\n        case .adminBan:\n            try Self.adminBan(view: view)\n        case .modAddToCommunity:\n            try Self.modAddToCommunity(view: view)\n        case .adminFeaturePostSite:\n            try Self.adminFeaturePostSite(view: view)\n        case .modFeaturePostCommunity:\n            try Self.modFeaturePostCommunity(view: view)\n        case .adminRemoveCommunity:\n            try Self.adminRemoveCommunity(view: view)\n\n        // These cases will not appear on Lemmy 1.0 \n\n        case .modFeaturePost, // Renamed to `.modFeaturePostCommunity`\n        .modRemoveCommunity, // Renamed to `.adminRemoveCommunity`\n        .modAddCommunity, // Renamed to `.modAddToCommunity`\n        .modAdd, // Renamed to `.adminAdd`\n        .modBan, // Renamed to `.adminBan`\n        .modHideCommunity, // Superceded by `.modChangeCommunityVisibility`\n        .all:\n            throw ApiClientError.featureUnsupported\n\n        // These cases are new in Lemmy 1.0, and do not yet have matching ModlogEntryContentSnapshot cases.\n        // Return `nil` rather than throwing so that the Modlog can still load. These cases will just be hidden.\n\n        case .adminAllowInstance, .adminBlockInstance, .modChangeCommunityVisibility,\n             .modLockComment, .modWarnPost, .modWarnComment:\n           nil\n        }\n\n        if let value {\n            self = value\n        } else {\n            return nil\n        }\n    }\n\n    private static func modRemovePost(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .modRemovePost)\n        guard let post = view.targetPost, let community = view.targetCommunity else {\n            throw ApiClientError.responseMissingRequiredData(\"modRemovePost target\")\n        }\n        return .removePost(\n            try .init(from: post),\n            community: try .init(from: community),\n            removed: !view.modlog.isRevert,\n            reason: view.modlog.reason\n        )\n    }\n\n    private static func modLockPost(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .modLockPost)\n        guard let post = view.targetPost, let community = view.targetCommunity else {\n            throw ApiClientError.responseMissingRequiredData(\"modLockPost target\")\n        }\n        return try .lockPost(\n            .init(from: post),\n            community: .init(from: community),\n            locked: !view.modlog.isRevert\n        )\n    }\n\n    private static func modRemoveComment(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .modRemoveComment)\n        guard let comment = view.targetComment,\n            let post = view.targetPost,\n            let community = view.targetCommunity,\n            let person = view.targetPerson else {\n            throw ApiClientError.responseMissingRequiredData(\n                \"modRemoveComment \\(view.targetPost == nil) \\(view.targetComment == nil) \\(view.targetPerson == nil)\"\n            )\n        }\n        return try .removeComment(\n            .init(from: comment),\n            creator: .init(from: person),\n            post: .init(from: post),\n            community: .init(from: community),\n            removed: !view.modlog.isRevert,\n            reason: view.modlog.reason\n        )\n    }\n\n    private static func modBanFromCommunity(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .modBanFromCommunity)\n        guard let community = view.targetCommunity, let person = view.targetPerson else {\n            throw ApiClientError.responseMissingRequiredData(\"modBanFromCommunity target\")\n        }\n        return try .banPersonFromCommunity(\n            person: .init(from: person),\n            community: .init(from: community),\n            banned: !view.modlog.isRevert,\n            reason: view.modlog.reason,\n            expires: view.modlog.expiresAt\n        )\n    }\n\n    private static func modTransferCommunity(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .modTransferCommunity)\n        guard let community = view.targetCommunity, let person = view.targetPerson else {\n            throw ApiClientError.responseMissingRequiredData(\"modTransferCommunity target\")\n        }\n        return try .transferCommunityOwnership(\n            person: .init(from: person),\n            community: .init(from: community)\n        )\n    }\n\n    private static func adminPurgePerson(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .adminPurgePerson)\n        return .purgePerson(reason: view.modlog.reason)\n    }\n\n    private static func adminPurgeCommunity(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .adminPurgeCommunity)\n        return .purgeCommunity(reason: view.modlog.reason)\n    }\n\n    private static func adminPurgePost(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .adminPurgePost)\n        return .purgePost(reason: view.modlog.reason)\n    }\n\n    private static func adminPurgeComment(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .adminPurgeComment)\n        return .purgeComment(reason: view.modlog.reason)\n    }\n\n    private static func adminAdd(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .adminAdd)\n        guard let person = view.targetPerson else {\n            throw ApiClientError.responseMissingRequiredData(\"adminAdd target\")\n        }\n        return try .updatePersonAdminStatus(\n            person: .init(from: person),\n            appointed: !view.modlog.isRevert\n        )\n    }\n\n    private static func adminBan(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .adminBan)\n        guard let person = view.targetPerson else {\n            throw ApiClientError.responseMissingRequiredData(\"adminBan target\")\n        }\n        return try .banPersonFromInstance(\n            person: .init(from: person),\n            banned: !view.modlog.isRevert,\n            reason: view.modlog.reason,\n            expires: view.modlog.expiresAt\n        )\n    }\n\n    private static func modAddToCommunity(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .modAddToCommunity)\n        guard let community = view.targetCommunity, let person = view.targetPerson else {\n            throw ApiClientError.responseMissingRequiredData(\"modAddToCommunity target\")\n        }\n        return try .updatePersonModeratorStatus(\n            person: .init(from: person),\n            community: .init(from: community),\n            appointed: !view.modlog.isRevert\n        )\n    }\n\n    private static func adminFeaturePostSite(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .adminFeaturePostSite)\n        guard let post = view.targetPost, let community = view.targetCommunity else {\n            throw ApiClientError.responseMissingRequiredData(\"adminFeaturePostSite target\")\n        }\n        return try .pinPost(\n            .init(from: post),\n            community: .init(from: community),\n            pinned: !view.modlog.isRevert,\n            type: .instance\n        )\n    }\n\n    private static func modFeaturePostCommunity(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .modFeaturePostCommunity)\n        guard let post = view.targetPost, let community = view.targetCommunity else {\n            throw ApiClientError.responseMissingRequiredData(\"modFeaturePostCommunity target\")\n        }\n        return try .pinPost(\n            .init(from: post),\n            community: .init(from: community),\n            pinned: !view.modlog.isRevert,\n            type: .community\n        )\n    }\n\n    private static func adminRemoveCommunity(view: LemmyModlogView) throws(ApiClientError) -> Self {\n        assert(view.modlog.kind == .adminRemoveCommunity)\n        guard let community = view.targetCommunity else {\n            throw ApiClientError.responseMissingRequiredData(\"adminRemoveCommunity target\")\n        }\n        return try .removeCommunity(\n            .init(from: community),\n            removed: !view.modlog.isRevert,\n            reason: view.modlog.reason\n        )\n    }\n}\n    \n\n// MARK: Lemmy 0.19\n\nextension ModlogEntryContentSnapshot {\n    init(from view: LemmyModRemovePostView) throws(ApiClientError) {\n        self = try .removePost(\n            .init(from: view.post),\n            community: .init(from: view.community),\n            removed: view.modRemovePost.removed,\n            reason: view.modRemovePost.reason\n        )\n    }\n    \n    init(from view: LemmyModLockPostView) throws(ApiClientError) {\n        self = try .lockPost(\n            .init(from: view.post),\n            community: .init(from: view.community),\n            locked: view.modLockPost.locked\n        )\n    }\n    \n    init(from view: LemmyModFeaturePostView) throws(ApiClientError) {\n        self = try .pinPost(\n            .init(from: view.post),\n            community: .init(from: view.community),\n            pinned: view.modFeaturePost.featured,\n            type: view.modFeaturePost.isFeaturedCommunity ? .community : .instance\n        )\n    }\n    \n    init(from view: LemmyAdminPurgePostView) throws(ApiClientError) {\n        self = .purgePost(reason: view.adminPurgePost.reason)\n    }\n    \n    init(from view: LemmyModRemoveCommentView) throws(ApiClientError) {\n        self = try .removeComment(\n            .init(from: view.comment),\n            creator: .init(from: view.commenter),\n            post: .init(from: view.post),\n            community: .init(from: view.community),\n            removed: view.modRemoveComment.removed,\n            reason: view.modRemoveComment.reason\n        )\n    }\n    \n    init(from view: LemmyAdminPurgeCommentView) throws(ApiClientError) {\n        self = .purgeComment(reason: view.adminPurgeComment.reason)\n    }\n    \n    init(from view: LemmyAdminRemoveCommunityView) throws(ApiClientError) {\n        self = try .removeCommunity(\n            .init(from: view.community),\n            removed: view.modRemoveCommunity.removed,\n            reason: view.modRemoveCommunity.reason\n        )\n    }\n    \n    init(from view: LemmyAdminPurgeCommunityView) throws(ApiClientError) {\n        self = .purgeCommunity(reason: view.adminPurgeCommunity.reason)\n    }\n    \n    init(from view: LemmyModHideCommunityView) throws(ApiClientError) {\n        self = try .hideCommunity(\n            .init(from: view.community),\n            hidden: view.modHideCommunity.hidden,\n            reason: view.modHideCommunity.reason\n        )\n    }\n    \n    init(from view: LemmyModTransferCommunityView) throws(ApiClientError) {\n        self = try .transferCommunityOwnership(\n            person: .init(from: view.moddedPerson),\n            community: .init(from: view.community)\n        )\n    }\n    \n    init(from view: LemmyModAddToCommunityView) throws(ApiClientError) {\n        self = try .updatePersonModeratorStatus(\n            person: .init(from: view.moddedPerson),\n            community: .init(from: view.community),\n            appointed: !view.modAddCommunity.removed\n        )\n    }\n    \n    init(from view: LemmyAdminAddView) throws(ApiClientError) {\n        self = try .updatePersonAdminStatus(\n            person: .init(from: view.moddedPerson),\n            appointed: !view.modAdd.removed\n        )\n    }\n    \n    init(from view: LemmyModBanFromCommunityView) throws(ApiClientError) {\n        self = try .banPersonFromCommunity(\n            person: .init(from: view.bannedPerson),\n            community: .init(from: view.community),\n            banned: view.modBanFromCommunity.banned,\n            reason: view.modBanFromCommunity.reason,\n            expires: view.modBanFromCommunity.expires\n        )\n    }\n    \n    init(from view: LemmyAdminBanView) throws(ApiClientError) {\n        self = try .banPersonFromInstance(\n            person: .init(from: view.bannedPerson),\n            banned: view.modBan.banned,\n            reason: view.modBan.reason,\n            expires: view.modBan.expires\n        )\n    }\n    \n    init(from view: LemmyAdminPurgePersonView) throws(ApiClientError) {\n        self = .purgePerson(reason: view.adminPurgePerson.reason)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/ModlogEntrySnapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-09-28.\n//\n\nimport Foundation\n\nextension ModlogEntrySnapshot {\n    init?(from view: LemmyModlogView) throws(ApiClientError) {\n        if let type = try ModlogEntryContentSnapshot(from: view) {\n            try self.init(\n                created: view.modlog.publishedAt,\n                moderator: view.moderator.map(Person1Snapshot.init),\n                type: type\n            )\n        } else {\n            return nil\n        }\n    }\n\n    init(from view: LemmyModRemovePostView) throws(ApiClientError) {\n        try self.init(\n            created: view.modRemovePost.when_,\n            moderator: view.moderator.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyModLockPostView) throws(ApiClientError) {\n        try self.init(\n            created: view.modLockPost.when_,\n            moderator: view.moderator.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyModFeaturePostView) throws(ApiClientError) {\n        try self.init(\n            created: view.modFeaturePost.when_,\n            moderator: view.moderator.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyAdminPurgePostView) throws(ApiClientError) {\n        try self.init(\n            created: view.adminPurgePost.when_,\n            moderator: view.admin.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyModRemoveCommentView) throws(ApiClientError) {\n        try self.init(\n            created: view.modRemoveComment.when_,\n            moderator: view.moderator.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyAdminPurgeCommentView) throws(ApiClientError) {\n        try self.init(\n            created: view.adminPurgeComment.when_,\n            moderator: view.admin.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyAdminRemoveCommunityView) throws(ApiClientError) {\n        try self.init(\n            created: view.modRemoveCommunity.when_,\n            moderator: view.moderator.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyAdminPurgeCommunityView) throws(ApiClientError) {\n        try self.init(\n            created: view.adminPurgeCommunity.when_,\n            moderator: view.admin.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyModHideCommunityView) throws(ApiClientError) {\n        try self.init(\n            created: view.modHideCommunity.when_,\n            moderator: view.admin.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyModTransferCommunityView) throws(ApiClientError) {\n        try self.init(\n            created: view.modTransferCommunity.when_,\n            moderator: view.moderator.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyModAddToCommunityView) throws(ApiClientError) {\n        try self.init(\n            created: view.modAddCommunity.when_,\n            moderator: view.moderator.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyAdminAddView) throws(ApiClientError) {\n        try self.init(\n            created: view.modAdd.when_,\n            moderator: view.moderator.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyModBanFromCommunityView) throws(ApiClientError) {\n        try self.init(\n            created: view.modBanFromCommunity.when_,\n            moderator: view.moderator.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyAdminBanView) throws(ApiClientError) {\n        try self.init(\n            created: view.modBan.when_,\n            moderator: view.moderator.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n    \n    init(from view: LemmyAdminPurgePersonView) throws(ApiClientError) {\n        try self.init(\n            created: view.adminPurgePerson.when_,\n            moderator: view.admin.map(Person1Snapshot.init),\n            type: .init(from: view)\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/PagedResponseUnion+Extensions.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-12-05.\n//  \n\nimport Foundation\n\nextension LemmyListCommentsResponseUnion {\n    var items: [LemmyCommentView] {\n        switch self {\n        case let .lemmyGetCommentsResponse(response): response.comments\n        case let .lemmyPagedResponse(response): response.items\n        }\n    }\n    \n    var nextPage: String? {\n        switch self {\n        case .lemmyGetCommentsResponse: nil\n        case let .lemmyPagedResponse(response): response.nextPage\n        }\n    }\n}\n\nextension LemmyListPostsResponseUnion {\n    var items: [LemmyPostView] {\n        switch self {\n        case let .lemmyGetPostsResponse(response): response.posts\n        case let .lemmyPagedResponse(response): response.items\n        }\n    }\n    \n    var nextPage: String? {\n        switch self {\n        case .lemmyGetPostsResponse: nil\n        case let .lemmyPagedResponse(response): response.nextPage\n        }\n    }\n}\n\nextension LemmyListCommentLikesResponseUnion {\n    var items: [LemmyVoteView] {\n        switch self {\n        case let .lemmyListCommentLikesResponse(response): response.commentLikes\n        case let .lemmyPagedResponse(response): response.items\n        }\n    }\n    \n    var nextPage: String? {\n        switch self {\n        case .lemmyListCommentLikesResponse: nil\n        case let .lemmyPagedResponse(response): response.nextPage\n        }\n    }\n}\n\nextension LemmyListPostLikesResponseUnion {\n    var items: [LemmyVoteView] {\n        switch self {\n        case let .lemmyListPostLikesResponse(response): response.postLikes\n        case let .lemmyPagedResponse(response): response.items\n        }\n    }\n    \n    var nextPage: String? {\n        switch self {\n        case .lemmyListPostLikesResponse: nil\n        case let .lemmyPagedResponse(response): response.nextPage\n        }\n    }\n}\n\nextension LemmyListCommunitiesResponseUnion {\n    var items: [LemmyCommunityView] {\n        switch self {\n        case let .lemmyListCommunitiesResponse(response): response.communities\n        case let .lemmyPagedResponse(response): response.items\n        }\n    }\n    \n    var nextPage: String? {\n        switch self {\n        case .lemmyListCommunitiesResponse: nil\n        case let .lemmyPagedResponse(response): response.nextPage\n        }\n    }\n}\n\nextension LemmyListRegistrationApplicationsResponseUnion {\n    var items: [LemmyRegistrationApplicationView] {\n        switch self {\n        case let .lemmyListRegistrationApplicationsResponse(response): response.registrationApplications\n        case let .lemmyPagedResponse(response): response.items\n        }\n    }\n    \n    var nextPage: String? {\n        switch self {\n        case .lemmyListRegistrationApplicationsResponse: nil\n        case let .lemmyPagedResponse(response): response.nextPage\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Person1Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Person1Snapshot {\n    init(from person: LemmyPerson) throws(ApiClientError) {\n        guard let actorId = person.apId ?? person.actorId else {\n            throw .responseMissingRequiredData(\"LemmyPerson actorId\")\n        }\n        \n        guard let published = person.publishedAt ?? person.published else {\n            throw .responseMissingRequiredData(\"LemmyPerson published\")\n        }\n\n        let instanceBan: InstanceBanType\n        if person.banned ?? false { // TODO: We should not be coalescing here! https://github.com/mlemgroup/mlem/issues/2049\n            if let expires = person.banExpires {\n                instanceBan = .temporarilyBanned(expires: expires)\n            } else {\n                instanceBan = .permanentlyBanned\n            }\n        } else {\n            instanceBan = .notBanned\n        }\n\n        self.init(\n            actorId: actorId,\n            id: person.id,\n            name: person.name,\n            created: published,\n            instanceId: person.instanceId,\n            displayName: person.displayName ?? person.name,\n            avatar: person.avatar,\n            banner: person.banner,\n            note: nil,\n            updated: person.updatedAt ?? person.updated,\n            description: person.bio,\n            matrixUserId: person.matrixUserId,\n            isBot: person.botAccount,\n            instanceBan: instanceBan,\n            deleted: person.deleted,\n            allPropertiesPresent: true\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Person2Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Person2Snapshot {\n    init(from person: LemmyPersonView) throws(ApiClientError) {\n        guard let postCount = person.person.postCount ?? person.counts?.postCount else {\n            throw .responseMissingRequiredData(\"LemmyPersonView postCount\")\n        }\n        \n        guard let commentCount = person.person.commentCount ?? person.counts?.commentCount else {\n            throw .responseMissingRequiredData(\"LemmyPersonView commentCount\")\n        }\n\n        try self.init(\n            person: .init(from: person.person),\n            isAdmin: person.isAdmin,\n            postCount: postCount,\n            commentCount: commentCount\n        )\n    }\n    \n    init(from localUser: LemmyLocalUserView) throws(ApiClientError) {\n        guard let postCount = localUser.person.postCount ?? localUser.counts?.postCount else {\n            throw .responseMissingRequiredData(\"LemmyLocalUserView postCount\")\n        }\n        \n        guard let commentCount = localUser.person.commentCount ?? localUser.counts?.commentCount else {\n            throw .responseMissingRequiredData(\"LemmyLocalUserView commentCount\")\n        }\n\n        try self.init(\n            person: .init(from: localUser.person),\n            isAdmin: localUser.localUser.admin,\n            postCount: postCount,\n            commentCount: commentCount\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Person3Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Person3Snapshot {\n    init(from userInfo: LemmyMyUserInfo) throws(ApiClientError) {\n        var moderatedCommunities: [Community1Snapshot] = []\n        moderatedCommunities.reserveCapacity(userInfo.moderates.count)\n        \n        for moderate in userInfo.moderates {\n            try moderatedCommunities.append(.init(from: moderate.community))\n        }\n        \n        self.init(\n            person: try .init(from: userInfo.localUserView),\n            site: nil,\n            moderatedCommunities: moderatedCommunities\n        )\n    }\n    \n    init(from personDetails: LemmyGetPersonDetailsResponse) throws(ApiClientError) {\n        var moderatedCommunities: [Community1Snapshot] = []\n        moderatedCommunities.reserveCapacity(personDetails.moderates.count)\n        \n        for moderate in personDetails.moderates {\n            try moderatedCommunities.append(.init(from: moderate.community))\n        }\n\n        self.init(\n            person: try .init(from: personDetails.personView),\n            site: try personDetails.site.map { site throws(ApiClientError) in try.init(from: site) },\n            moderatedCommunities: moderatedCommunities\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Person4Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Person4Snapshot {\n    init(from userInfo: LemmyMyUserInfo) throws(ApiClientError) {\n        let user = userInfo.localUserView.localUser\n\n        guard let showScores = (user.showScore ?? user.showScores) else {\n            throw .responseMissingRequiredData(\"LemmyMyUserInfo showScores\")\n        }\n\n        try self.init(\n            person: .init(from: userInfo),\n            email: user.email,\n            showNsfw: user.showNsfw,\n            theme: user.theme,\n            defaultListingType: .init(from: user.defaultListingType),\n            interfaceLanguage: user.interfaceLanguage,\n            showAvatars: user.showAvatars,\n            sendNotificationsToEmail: user.sendNotificationsToEmail,\n            showScores: showScores,\n            showBotAccounts: user.showBotAccounts,\n            showReadPosts: user.showReadPosts,\n            discussionLanguageIds: .init(userInfo.discussionLanguages.filter { $0 != 0 }),\n            emailVerified: user.emailVerified,\n            acceptedApplication: user.acceptedApplication,\n            openLinksInNewTab: user.openLinksInNewTab,\n            blurNsfw: user.blurNsfw,\n            autoExpandImages: user.autoExpand,\n            infiniteScrollEnabled: user.infiniteScrollEnabled,\n            postListingMode: .init(from: user.postListingMode),\n            totp2faEnabled: user.totp2faEnabled,\n            enableKeyboardNavigation: user.enableKeyboardNavigation,\n            enableAnimatedImages: user.enableAnimatedImages,\n            collapseBotComments: user.collapseBotComments\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/PersonVoteSnapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-09-18.\n//\n\nimport Foundation\n\nextension PersonVoteSnapshot {\n    init(from vote: LemmyVoteView) throws(ApiClientError) {\n        let score: Int?\n        if let isUpvote = vote.isUpvote {\n            score = isUpvote ? 1 : -1\n        } else {\n            score = vote.score\n        }\n\n        guard let score else {\n            throw .responseMissingRequiredData(\"LemmyVoteView score\")\n        }\n\n        try self.init(\n            creator: .init(from: vote.creator),\n            score: score,\n            creatorBannedFromCommunity: vote.creatorBannedFromCommunity\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/PersonalUnreadCountSnapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension PersonalUnreadCountSnapshot {\n    init(from response: LemmyGetUnreadCountResponse) throws(ApiClientError) {\n        self.replies = response.replies\n        self.mentions = response.mentions\n        self.messages = response.privateMessages\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Post1Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Post1Snapshot {\n    init(from post: LemmyPost) throws(ApiClientError) {\n        guard let published = post.publishedAt ?? post.published else {\n            throw .responseMissingRequiredData(\"LemmyPost published\")\n        }\n\n        self.init(\n            actorId: post.apId,\n            id: post.id,\n            creatorId: post.creatorId,\n            communityId: post.communityId,\n            created: published,\n            title: post.name,\n            content: post.body,\n            linkUrl: post.linkUrl,\n            embed: post.embed,\n            poll: nil,\n            nsfw: post.nsfw,\n            thumbnailUrl: post.thumbnailImageUrl,\n            updated: post.updatedAt ?? post.updated,\n            languageId: post.languageId,\n            altText: post.altText,\n            deleted: post.deleted,\n            removed: post.removed,\n            pinnedCommunity: post.featuredCommunity,\n            pinnedInstance: post.featuredLocal,\n            locked: post.locked\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Post2Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Post2Snapshot {\n    /// Instantiates a Post2Snapshot from a given LemmyPostView\n    /// - Parameters:\n    ///   - post: LemmyPostView\n    ///   - overrideRead: if present, overrides the LemmyPostView's read value. This is required because Lemmy doesn't return `read: true` in some cases (e.g., save post) even if the value is updated server-side.\n    init(from post: LemmyPostView, overrideRead: Bool? = nil) throws(ApiClientError) {\n        let votes: VotesModel\n        if let counts = post.counts {\n            votes = .init(from: counts, myVote: .guaranteedInit(from: post.myVote))\n        } else if let upvotes = post.post.upvotes, let downvotes = post.post.downvotes {\n            votes = .init(\n                upvotes: upvotes,\n                downvotes: downvotes,\n                myVote: .init(post.postActions?.voteIsUpvote)\n            )\n        } else {\n            throw .responseMissingRequiredData(\"LemmyPostView scores\")\n        }\n        \n        let creatorBlocked: Bool\n        if let personActions = post.personActions {\n            creatorBlocked = personActions.blockedAt != nil\n        } else if let creatorBlocked_ = post.creatorBlocked {\n            creatorBlocked = creatorBlocked_\n        } else {\n            // `personActions` is `nil` on Lemmy 1.0 for your own posts.\n            // Therefore we can set `creatorBlocked` to `false`.\n            creatorBlocked = false\n        }\n\n        let commentCount: Int\n        let unreadCommentCount: Int\n        if let comments = post.post.comments {\n            commentCount = comments\n            unreadCommentCount = comments - (post.postActions?.readCommentsAmount ?? 0)\n        } else if let counts = post.counts, let unreadComments = post.unreadComments {\n            commentCount = counts.comments\n            unreadCommentCount = unreadComments\n        } else {\n            throw .responseMissingRequiredData(\"LemmyPostView commentCount\")\n        }\n\n        let saved: Bool\n        let read: Bool\n        let hidden: Bool\n        if let saved_ = post.saved, let read_ = post.read, let hidden_ = post.hidden {\n            saved = saved_\n            read = overrideRead ?? read_\n            hidden = hidden_\n        } else {\n            let actions = post.postActions\n            saved = actions?.savedAt != nil\n            read = overrideRead ?? (actions?.readAt != nil)\n            hidden = actions?.hiddenAt != nil\n        }\n\n        try self.init(\n            post: .init(from: post.post),\n            creator: .init(from: post.creator),\n            community: .init(from: post.community),\n            commentCount: commentCount,\n            unreadCommentCount: unreadCommentCount,\n            creatorIsModerator: post.creatorIsModerator,\n            creatorIsAdmin: post.creatorIsAdmin,\n            creatorBannedFromCommunity: post.creatorBannedFromCommunity,\n            creatorBlocked: creatorBlocked,\n            votes: votes,\n            saved: saved,\n            read: read,\n            hidden: hidden\n        )\n    }\n    \n    init(from report: LemmyPostReportView) throws(ApiClientError) {\n        let votes: VotesModel\n        if let counts = report.counts {\n            votes = .init(from: counts, myVote: .init(report.postActions?.voteIsUpvote))\n        } else if let upvotes = report.post.upvotes, let downvotes = report.post.downvotes {\n            votes = .init(\n                upvotes: upvotes,\n                downvotes: downvotes,\n                myVote: .init(report.postActions?.voteIsUpvote)\n            )\n        } else {\n            throw .responseMissingRequiredData(\"LemmyPostReportView scores\")\n        }\n        \n        guard let creatorBlocked = report.creatorBlocked else {\n            throw .responseMissingRequiredData(\"LemmyPostReportView creatorBlocked\")\n        }\n        \n        let commentCount: Int\n        let unreadCommentCount: Int\n        if let actions = report.postActions, let comments = report.post.comments {\n            commentCount = comments\n            unreadCommentCount = comments - (actions.readCommentsAmount ?? 0)\n        } else if let counts = report.counts, let unreadComments = report.unreadComments {\n            commentCount = counts.comments\n            unreadCommentCount = unreadComments\n        } else {\n            throw .responseMissingRequiredData(\"LemmyPostReportView commentCount\")\n        }\n\n        let saved: Bool\n        let read: Bool\n        let hidden: Bool\n        if let actions = report.postActions {\n            saved = actions.savedAt != nil\n            read = actions.readAt != nil\n            hidden = actions.hiddenAt != nil\n        } else if let saved_ = report.saved, let read_ = report.read, let hidden_ = report.hidden {\n            saved = saved_\n            read = read_\n            hidden = hidden_\n        } else {\n            throw .responseMissingRequiredData(\"LemmyPostReportView actions\")\n        }\n\n        try self.init(\n            post: .init(from: report.post),\n            creator: .init(from: report.postCreator),\n            community: .init(from: report.community),\n            commentCount: commentCount,\n            unreadCommentCount: unreadCommentCount,\n            creatorIsModerator: report.creatorIsModerator ?? false,\n            creatorIsAdmin: report.creatorIsAdmin ?? false,\n            creatorBannedFromCommunity: report.creatorBannedFromCommunity,\n            creatorBlocked: creatorBlocked,\n            votes: votes,\n            saved: saved,\n            read: read,\n            hidden: hidden\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/Post3Snapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension Post3Snapshot {\n    init(from post: LemmyGetPostResponse) throws(ApiClientError) {\n        var crossPosts: [Post2Snapshot] = []\n        for crossPost in post.crossPosts {\n            try crossPosts.append(.init(from: crossPost))\n        }\n\n        try self.init(\n            post: .init(from: post.postView),\n            community: .init(from: post.communityView),\n            crossPosts: crossPosts\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/PostFeatureType+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-20.\n//\n\nimport Foundation\n\nextension PostFeatureType {\n    var apiType: LemmyPostFeatureType {\n        switch self {\n        case .community: .community\n        case .instance: .local\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/PostSortType+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\n// This is reluncantly public because Mlem uses it; this should really be `internal`\npublic extension PostSortType {\n    init(_ legacyApiSortType: LemmySortType) {\n        switch legacyApiSortType {\n        case .active: self = .active\n        case .hot: self = .hot\n        case .new: self = .new\n        case .old: self = .old\n        case .mostComments: self = .mostComments\n        case .newComments: self = .newComments\n        case .controversial: self = .controversial\n        case .scaled: self = .scaled\n        default:\n            if let timeRange = SortTimeRange(legacyApiSortType) {\n                self = .top(timeRange)\n            } else {\n                assertionFailure()\n                self = .top(.allTime)\n            }\n        }\n    }\n    \n    internal func apiType(for endpoint: LemmyEndpointVersion) throws(ApiClientError) -> LemmySearchSortTypeBridge {\n        switch endpoint {\n        case .v3: try .oldOrUnsupported(v3ApiType)\n        case .v4: try .newOrUnsupported(v4SearchApiType)\n        }\n    }\n    \n    internal func apiType(for endpoint: LemmyEndpointVersion) throws(ApiClientError) -> LemmyPostSortTypeBridge {\n        switch endpoint {\n        case .v3: try .oldOrUnsupported(v3ApiType)\n        case .v4: .new(v4PostApiType)\n        }\n    }\n\n    var v3ApiType: LemmySortType? {\n        switch self {\n        case .active: .active\n        case .hot: .hot\n        case .new: .new\n        case .old: .old\n        case .mostComments: .mostComments\n        case .newComments: .newComments\n        case .controversial: .controversial\n        case .scaled: .scaled\n        case let .top(timeRange): timeRange.legacyApiSortType\n        }\n    }\n    \n    var v4PostApiType: LemmyPostSortType {\n        switch self {\n        case .active: .active\n        case .hot: .hot\n        case .new: .new\n        case .old: .old\n        case .mostComments: .mostComments\n        case .newComments: .newComments\n        case .controversial: .controversial\n        case .scaled: .scaled\n        case .top: .top\n        }\n    }\n    \n    var v4SearchApiType: LemmySearchSortType? {\n        switch self {\n        case .new: .new\n        case .old: .old\n        case .top: .top\n        default: nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/RegistrationApplicationSnapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-09-24.\n//\n\nimport Foundation\n\nextension RegistrationApplicationSnapshot {\n    init(from application: LemmyRegistrationApplicationView) throws(ApiClientError) {\n        guard let published = application.registrationApplication.publishedAt ?? application.registrationApplication.published else {\n            throw .responseMissingRequiredData(\"LemmyRegistrationApplication published\")\n        }\n        \n        let resolution: RegistrationApplication.ResolutionState\n        if application.creatorLocalUser.acceptedApplication {\n            resolution = .approved\n        } else if application.admin != nil {\n            resolution = .denied(reason: application.registrationApplication.denyReason)\n        } else {\n            resolution = .unresolved\n        }\n\n        try self.init(\n            id: application.registrationApplication.id,\n            created: published,\n            questionResponse: application.registrationApplication.answer,\n            email: application.creatorLocalUser.email,\n            showNsfw: application.creatorLocalUser.showNsfw,\n            creator: .init(from: application.creator),\n            emailVerified: application.creatorLocalUser.emailVerified,\n            resolver: application.admin.map { admin throws(ApiClientError) in try .init(from: admin) },\n            resolution: resolution\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/RegistrationMode+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension RegistrationMode {\n    init(from mode: LemmyRegistrationMode) {\n        self = switch mode {\n        case .closed: .closed\n        case .requireApplication: .requiresApplication\n        case .open: .open\n        }\n    }\n    \n    var apiType: LemmyRegistrationMode {\n        switch self {\n        case .closed: .closed\n        case .open: .open\n        case .requiresApplication: .requireApplication\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/ReportSnapshot+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-25.\n//\n\nimport Foundation\n\nextension ReportSnapshot {\n    init(from report: LemmyCommentReportView) throws(ApiClientError) {\n        guard let published = report.commentReport.publishedAt ?? report.commentReport.published else {\n            throw .responseMissingRequiredData(\"LemmyCommentReportView published\")\n        }\n\n        try self.init(\n            creator: .init(from: report.creator),\n            id: report.commentReport.id,\n            created: published,\n            resolver: report.resolver.map { resolver throws(ApiClientError) in try .init(from: resolver) },\n            updated: report.commentReport.updatedAt ?? report.commentReport.updated,\n            resolved: report.commentReport.resolved,\n            reason: report.commentReport.reason,\n            target: .comment(.init(from: report))\n        )\n    }\n    \n    init(from report: LemmyPostReportView) throws(ApiClientError) {\n        guard let published = report.postReport.publishedAt ?? report.postReport.published else {\n            throw .responseMissingRequiredData(\"LemmyPostReply published\")\n        }\n\n        try self.init(\n            creator: .init(from: report.creator),\n            id: report.postReport.id,\n            created: published,\n            resolver: report.resolver.map { resolver throws(ApiClientError) in try .init(from: resolver) },\n            updated: report.postReport.updatedAt ?? report.postReport.updated,\n            resolved: report.postReport.resolved,\n            reason: report.postReport.reason,\n            target: .post(.init(from: report))\n        )\n    }\n    \n    init(from report: LemmyPrivateMessageReportView) throws(ApiClientError) {\n        guard let published = report.privateMessageReport.publishedAt ?? report.privateMessageReport.published else {\n            throw .responseMissingRequiredData(\"LemmyPrivateMessageReport published\")\n        }\n        let messageView = report.toPrivateMessageView()\n\n        try self.init(\n            creator: .init(from: report.creator),\n            id: report.privateMessageReport.id,\n            created: published,\n            resolver: report.resolver.map { resolver throws(ApiClientError) in try .init(from: resolver) },\n            updated: report.privateMessageReport.updatedAt ?? report.privateMessageReport.updated,\n            resolved: report.privateMessageReport.resolved,\n            reason: report.privateMessageReport.reason,\n            target: .message(.init(from: messageView))\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/ResolvedContent+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension ResolvedContent {\n    init(from response: LemmyResolveObjectResponseUnion) throws(ApiClientError) {\n        switch response {\n        case let .lemmyResolveObjectResponse(value):\n            try self.init(from: value)\n        case let .lemmyResolveObjectView(value):\n            try self.init(from: value)\n        }\n    }\n    \n    init(from response: LemmyResolveObjectResponse) throws(ApiClientError) {\n        if let comment = response.comment {\n            self = try .comment(.init(from: comment))\n        } else if let post = response.post {\n            self = try .post(.init(from: post))\n        } else if let community = response.community {\n            self = try .community(.init(from: community))\n        } else if let person = response.person {\n            self = try .person(.init(from: person))\n        } else {\n            throw .noEntityFound\n        }\n    }\n    \n    init(from response: LemmyResolveObjectView) throws(ApiClientError) {\n        // This initializer is only used in 1.0.0 onwards, so we only need\n        // to consider the `results` array and not the other arrays (which\n        // are only used prior to 1.0.0)\n        switch response {\n        case let .comment(comment):\n            self = try .comment(.init(from: comment))\n        case let .community(community):\n            self = try .community(.init(from: community))\n        case .multiCommunity:\n            throw .featureUnsupported\n        case let .person(person):\n            self = try .person(.init(from: person))\n        case let .post(post):\n            self = try .post(.init(from: post))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/SearchSortType+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension SearchSortType {\n    init?(_ legacyApiSortType: LemmySortType) {\n        switch legacyApiSortType {\n        case .new:\n            self = .new\n        case .old:\n            self = .old\n        default:\n            if let timeRange = SortTimeRange(legacyApiSortType) {\n                self = .top(timeRange)\n            } else {\n                return nil\n            }\n        }\n    }\n    \n    func apiType(for endpoint: LemmyEndpointVersion) throws(ApiClientError) -> LemmySearchSortTypeBridge {\n        switch endpoint {\n        case .v3: try .oldOrUnsupported(v3ApiType)\n        case .v4: try .newOrUnsupported(v4ApiType)\n        }\n    }\n    \n    private var v3ApiType: LemmySortType? {\n        switch self {\n        case .new: .new\n        case .old: .old\n        case let .top(timeRange): timeRange.legacyApiSortType\n        }\n    }\n    \n    private var v4ApiType: LemmySearchSortType? {\n        switch self {\n        case .new: .new\n        case .old: .old\n        case .top: .top\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/LemmyExtensions/SortTimeRange+Lemmy.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-14.\n//\n\nimport Foundation\n\nextension SortTimeRange {\n    var legacyApiSortType: LemmySortType? {\n        switch self {\n        case .allTime: .topAll\n        case let .limited(timeInterval): LegacySortTimeRangeLimit(timeInterval)?.legacyApiSortType\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ListingType.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-11.\n//\n\nimport Foundation\n\npublic enum ListingType: String, CaseIterable, Codable {\n    case all, local, subscribed, moderated, popular, suggested\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Message/Message1.swift",
    "content": "//\n//  Message1.swift\n//\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport Foundation\nimport Observation\n\n@Observable\npublic final class Message1: Message1Providing {\n    public var api: ApiClient\n    public var message1: Message1 { self }\n    \n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let creatorId: Int\n    public let recipientId: Int\n    public var content: String\n    public let created: Date\n    public var updated: Date?\n    public let isOwnMessage: Bool\n    \n    var deletedManager: StateManager<Bool>\n    public var deleted: Bool { deletedManager.displayedValue }\n    \n    init(\n        api: ApiClient,\n        actorId: ActorIdentifier,\n        id: Int,\n        creatorId: Int,\n        recipientId: Int,\n        isOwnMessage: Bool,\n        content: String,\n        deleted: Bool,\n        created: Date,\n        updated: Date?,\n    ) {\n        self.api = api\n        self.actorId = actorId\n        self.id = id\n        self.creatorId = creatorId\n        self.recipientId = recipientId\n        self.isOwnMessage = isOwnMessage\n        self.content = content\n        self.deletedManager = .init(wrappedValue: deleted)\n        self.created = created\n        self.updated = updated\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Message/Message1Providing+Snapshots.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-10-12.\n//\n\nimport Foundation\n\nextension Message1Providing {\n    func takeSnapshot1() -> Message1Snapshot {\n        .init(\n            actorId: actorId,\n            id: id,\n            creatorId: creatorId,\n            recipientId: recipientId,\n            created: created,\n            content: content,\n            updated: updated,\n            read: false,\n            deleted: deleted\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Message/Message1Providing.swift",
    "content": "//\n//  Message1Providing.swift\n//\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport Foundation\n\npublic protocol Message1Providing:\n    ContentModel,\n    ActorIdentifiable,\n    ContentIdentifiable,\n    DeletableProviding,\n    ReportableProviding,\n    SelectableContentProviding {\n    var message1: Message1 { get }\n    \n    var id: Int { get }\n    var creatorId: Int { get }\n    var recipientId: Int { get }\n    var content: String { get }\n    var deleted: Bool { get }\n    var created: Date { get }\n    var updated: Date? { get }\n    \n    var id_: Int? { get }\n    var creatorId_: Int? { get }\n    var recipientId_: Int? { get }\n    var content_: String? { get }\n    var deleted_: Bool? { get }\n    var created_: Date? { get }\n    var updated_: Date? { get }\n    \n    // From Message2Providing\n    var creator_: Person? { get }\n    var recipient_: Person? { get }\n}\n\npublic typealias Message = Message1Providing\n\n// SelectableContentProviding conformance\npublic extension Message1Providing {\n    var selectableContent: String? { content }\n}\n\npublic extension Message1Providing {\n    static var modelTypeId: ContentType { .message }\n    \n    var actorId: ActorIdentifier { message1.actorId }\n    var id: Int { message1.id }\n    var creatorId: Int { message1.creatorId }\n    var recipientId: Int { message1.recipientId }\n    var content: String { message1.content }\n    var deleted: Bool { message1.deleted }\n    var created: Date { message1.created }\n    var updated: Date? { message1.updated }\n    var isOwnMessage: Bool { message1.isOwnMessage }\n    \n    var id_: Int? { message1.id }\n    var creatorId_: Int? { message1.creatorId }\n    var recipientId_: Int? { message1.recipientId }\n    var content_: String? { message1.content }\n    var deleted_: Bool? { message1.deleted }\n    var created_: Date? { message1.created }\n    var updated_: Date? { message1.updated }\n    var isOwnMessage_: Bool? { message1.isOwnMessage }\n    \n    var creator_: Person? { nil }\n    var recipient_: Person? { nil }\n}\n\n// ReportableProviding conformance\npublic extension Message1Providing {\n    func isOwnContent(myPersonId: Int) -> Bool { isOwnMessage }\n}\n\npublic extension Message1Providing {\n    private var deletedManager: StateManager<Bool> { message1.deletedManager }\n    \n    func reply(content: String) async throws -> Message2 {\n        try await api.createMessage(personId: recipientId, content: content)\n    }\n    \n    func report(reason: String) async throws {\n        try await api.reportMessage(id: id, reason: reason)\n    }\n    \n    func updateDeleted(_ newValue: Bool, callback: ((UpdateStatus) -> Void)?) {\n        // TODO: UpdateQueue queued state management\n        _ = deletedManager.performRequest(expectedResult: newValue) { semaphore in\n            do {\n                try await self.api.deleteMessage(id: self.id, delete: newValue, semaphore: semaphore)\n                callback?(.success)\n            } catch {\n                callback?(.failure(error))\n            }\n        }\n    }\n    \n    func edit(content: String) async throws {\n        try await api.editMessage(id: id, content: content)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Message/Message2.swift",
    "content": "//\n//  Message2.swift\n//\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport Foundation\n\n@Observable\npublic final class Message2: Message2Providing, FeedLoadable {\n    public typealias FilterType = InboxItemFilterType\n    \n    public func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        switch sortType {\n        case .new:\n            return .new(created)\n        }\n    }\n    \n    public var api: ApiClient\n    public var message2: Message2 { self }\n    \n    public let message1: Message1\n    public let creator: Person\n    public let recipient: Person\n    \n    init(\n        api: ApiClient,\n        message1: Message1,\n        creator: Person,\n        recipient: Person\n    ) {\n        self.api = api\n        self.message1 = message1\n        self.creator = creator\n        self.recipient = recipient\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Message/Message2Providing+Snapshots.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-10-12.\n//\n\nimport Foundation\n\nextension Message2Providing {\n    func takeSnapshot2() -> Message2Snapshot {\n        .init(\n            message: message1.takeSnapshot1(),\n            creator: creator.takeSnapshot1(),\n            recipient: recipient.takeSnapshot1()\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Message/Message2Providing.swift",
    "content": "//\n//  Message2Providing.swift\n//\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport Foundation\n\npublic protocol Message2Providing: Message1Providing, ActorIdentifiable {\n    var message2: Message2 { get }\n    \n    var creator: Person { get }\n    var recipient: Person { get }\n}\n\npublic extension Message2Providing {\n    var message1: Message1 { message2.message1 }\n    \n    var creator: Person { message2.creator }\n    var recipient: Person { message2.recipient }\n    \n    var creator_: Person? { message2.creator }\n    var recipient_: Person? { message2.recipient }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Modlog/ModlogEntry.swift",
    "content": "//\n//  ModlogEntry.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-23.\n//\n\nimport Foundation\n\npublic struct ModlogEntry {\n    public let api: ApiClient\n    public let created: Date\n    public let moderator: Person?\n    public let type: ModlogEntryContent\n}\n\nextension ModlogEntry: FeedLoadable {\n    public typealias FilterType = ModlogEntryFilterType\n    \n    public func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        switch sortType {\n        case .new:\n            return .new(created)\n        }\n    }\n    \n    public static func == (lhs: ModlogEntry, rhs: ModlogEntry) -> Bool {\n        lhs.created == rhs.created && lhs.type == rhs.type\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Modlog/ModlogEntryContent.swift",
    "content": "//\n//  ModlogEntryContent.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-25.\n//\n\nimport Foundation\n\npublic enum ModlogEntryContent: Equatable {\n    case removePost(\n        _ post: Post,\n        community: Community,\n        removed: Bool,\n        reason: String?\n    )\n    case lockPost(\n        _ post: Post,\n        community: Community,\n        locked: Bool\n    )\n    case pinPost(\n        _ post: Post,\n        community: Community,\n        pinned: Bool,\n        type: PostFeatureType\n    )\n    case purgePost(reason: String?)\n    \n    case removeComment(\n        _ comment: Comment,\n        creator: Person,\n        post: Post,\n        community: Community,\n        removed: Bool,\n        reason: String?\n    )\n    case purgeComment(reason: String?)\n    \n    case removeCommunity(\n        _ community: Community,\n        removed: Bool,\n        reason: String?\n    )\n    case purgeCommunity(reason: String?)\n    \n    case hideCommunity(\n        _ community: Community,\n        hidden: Bool,\n        reason: String?\n    )\n    case transferCommunityOwnership(\n        person: Person,\n        community: Community\n    )\n    \n    case updatePersonModeratorStatus(\n        person: Person,\n        community: Community,\n        appointed: Bool\n    )\n    case updatePersonAdminStatus(\n        person: Person,\n        appointed: Bool\n    )\n    case banPersonFromCommunity(\n        person: Person,\n        community: Community,\n        banned: Bool,\n        reason: String?,\n        expires: Date?\n    )\n    case banPersonFromInstance(\n        person: Person,\n        banned: Bool,\n        reason: String?,\n        expires: Date?\n    )\n    case purgePerson(reason: String?)\n    \n    public var community: Community? {\n        switch self {\n        case let .removePost(_, community, _, _): community\n        case let .lockPost(_, community, _): community\n        case let .pinPost(_, community, _, _): community\n        case let .removeComment(_, _, _, community, _, _): community\n        case let .removeCommunity(community, _, _): community\n        case let .hideCommunity(community, _, _): community\n        case let .transferCommunityOwnership(_, community): community\n        case let .updatePersonModeratorStatus(_, community, _): community\n        case let .banPersonFromCommunity(_, community, _, _, _): community\n        default: nil\n        }\n    }\n    \n    public var type: ModlogEntryType {\n        switch self {\n        case .removePost: .removePost\n        case .lockPost: .lockPost\n        case .pinPost: .pinPost\n        case .purgePost: .purgePost\n        case .removeComment: .removeComment\n        case .purgeComment: .purgeComment\n        case .removeCommunity: .removeCommunity\n        case .purgeCommunity: .purgeCommunity\n        case .hideCommunity: .hideCommunity\n        case .transferCommunityOwnership: .transferCommunityOwnership\n        case .updatePersonModeratorStatus: .updatePersonModeratorStatus\n        case .updatePersonAdminStatus: .updatePersonAdminStatus\n        case .banPersonFromCommunity: .banPersonFromCommunity\n        case .banPersonFromInstance: .banPersonFromInstance\n        case .purgePerson: .purgePerson\n        }\n    }\n    \n    @MainActor\n    init(from snapshot: ModlogEntryContentSnapshot, api: ApiClient) {\n        switch snapshot {\n        case let .removePost(post, community, removed, reason):\n            self = .removePost(\n                api.caches.post.getModel(api: api, from: .post1(post)),\n                community: api.caches.community.getModel(api: api, from: .community1(community)),\n                removed: removed,\n                reason: reason\n            )\n        case let .lockPost(post, community, locked):\n            self = .lockPost(\n                api.caches.post.getModel(api: api, from: .post1(post)),\n                community: api.caches.community.getModel(api: api, from: .community1(community)),\n                locked: locked\n            )\n        case let .pinPost(post, community, pinned, type):\n            self = .pinPost(\n                api.caches.post.getModel(api: api, from: .post1(post)),\n                community: api.caches.community.getModel(api: api, from: .community1(community)),\n                pinned: pinned,\n                type: type\n            )\n        case let .purgePost(reason):\n            self = .purgePost(reason: reason)\n        case let .removeComment(comment, creator, post, community, removed, reason):\n            self = .removeComment(\n                api.caches.comment.getModel(api: api, from: .comment1(comment)),\n                creator: api.caches.person.getModel(api: api, from: .person1(creator)),\n                post: api.caches.post.getModel(api: api, from: .post1(post)),\n                community: api.caches.community.getModel(api: api, from: .community1(community)),\n                removed: removed,\n                reason: reason\n            )\n        case let .purgeComment(reason):\n            self = .purgeComment(reason: reason)\n        case let .removeCommunity(community, removed, reason):\n            self = .removeCommunity(\n                api.caches.community.getModel(api: api, from: .community1(community)),\n                removed: removed,\n                reason: reason\n            )\n        case let .purgeCommunity(reason):\n            self = .purgeCommunity(reason: reason)\n        case let .hideCommunity(community, hidden, reason):\n            self = .hideCommunity(\n                api.caches.community.getModel(api: api, from: .community1(community)),\n                hidden: hidden,\n                reason: reason\n            )\n        case let .transferCommunityOwnership(person, community):\n            self = .transferCommunityOwnership(\n                person: api.caches.person.getModel(api: api, from: .person1(person)),\n                community: api.caches.community.getModel(api: api, from: .community1(community))\n            )\n        case let .updatePersonModeratorStatus(person, community, appointed):\n            self = .updatePersonModeratorStatus(\n                person: api.caches.person.getModel(api: api, from: .person1(person)),\n                community: api.caches.community.getModel(api: api, from: .community1(community)),\n                appointed: appointed\n            )\n        case let .updatePersonAdminStatus(person, appointed):\n            self = .updatePersonAdminStatus(\n                person: api.caches.person.getModel(api: api, from: .person1(person)),\n                appointed: appointed\n            )\n        case let .banPersonFromCommunity(person, community, banned, reason, expires):\n            self = .banPersonFromCommunity(\n                person: api.caches.person.getModel(api: api, from: .person1(person)),\n                community: api.caches.community.getModel(api: api, from: .community1(community)),\n                banned: banned,\n                reason: reason,\n                expires: expires\n            )\n        case let .banPersonFromInstance(person, banned, reason, expires):\n            self = .banPersonFromInstance(\n                person: api.caches.person.getModel(api: api, from: .person1(person)),\n                banned: banned,\n                reason: reason,\n                expires: expires\n            )\n        case let .purgePerson(reason):\n            self = .purgePerson(reason: reason)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ModlogEntryType.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic enum ModlogEntryType: CaseIterable {\n    case removePost\n    case lockPost\n    case pinPost\n    case purgePost\n    case removeComment\n    case purgeComment\n    case removeCommunity\n    case purgeCommunity\n    case hideCommunity\n    case transferCommunityOwnership\n    case updatePersonModeratorStatus\n    case updatePersonAdminStatus\n    case banPersonFromCommunity\n    case banPersonFromInstance\n    case purgePerson\n    \n    init?(from type: LemmyModlogKind) throws(ApiClientError) {\n        let result: Self? = switch type {\n        case .all: nil\n        case .modRemovePost: .removePost\n        case .modLockPost: .lockPost\n        case .modFeaturePost: .pinPost\n        case .modRemoveComment: .removeComment\n        case .modBanFromCommunity: .banPersonFromCommunity\n        case .modAddToCommunity, .modAddCommunity: .updatePersonModeratorStatus\n        case .modTransferCommunity: .transferCommunityOwnership\n        case .modHideCommunity: .hideCommunity\n        case .adminAdd, .modAdd: .updatePersonAdminStatus\n        case .adminBan, .modBan: .banPersonFromInstance\n        case .adminRemoveCommunity, .modRemoveCommunity: .removeCommunity\n        case .adminPurgePerson: .purgePerson\n        case .adminPurgeCommunity: .purgeCommunity\n        case .adminPurgePost: .purgePost\n        case .adminPurgeComment: .purgeComment\n        case .modChangeCommunityVisibility: throw .featureUnsupported\n        case .adminBlockInstance: throw .featureUnsupported\n        case .adminAllowInstance: throw .featureUnsupported\n        case .modLockComment: throw .featureUnsupported\n        case .adminFeaturePostSite: throw .featureUnsupported\n        case .modFeaturePostCommunity: throw .featureUnsupported\n        case .modWarnPost: throw .featureUnsupported\n        case .modWarnComment: throw .featureUnsupported\n        }\n        if let result {\n            self = result\n        } else {\n            return nil\n        }\n    }\n    \n    var apiType: LemmyModlogKind {\n        switch self {\n        case .removePost: .modRemovePost\n        case .lockPost: .modLockPost\n        case .pinPost: .modFeaturePost\n        case .purgePost: .adminPurgePost\n        case .removeComment: .modRemoveComment\n        case .purgeComment: .adminPurgeComment\n        case .removeCommunity: .modRemoveCommunity\n        case .purgeCommunity: .adminPurgeCommunity\n        case .hideCommunity: .modHideCommunity\n        case .transferCommunityOwnership: .modTransferCommunity\n        case .updatePersonModeratorStatus: .modAddCommunity\n        case .updatePersonAdminStatus: .modAdd\n        case .banPersonFromCommunity: .modBanFromCommunity\n        case .banPersonFromInstance: .modBan\n        case .purgePerson: .adminPurgePerson\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Notification/InboxNotification+Snapshots.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-10-12.\n//\n\nimport Foundation\n\nextension InboxNotification {\n    @MainActor\n    func snapshotUpdate(with snapshot: InboxNotificationSnapshot, isResultOfTask: Bool) {\n        switch self.content {\n        case let .message(message) where message.isOwnMessage:\n            break\n        default:\n            setIfChanged(\\.read, snapshot.read)\n        }\n    }\n    \n    func takeSnapshot() -> InboxNotificationSnapshot? {\n        guard let snapshot = content.takeSnapshot() else { return nil }\n        return .init(\n            id: id,\n            contentId: contentId,\n            read: read,\n            content: snapshot\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Notification/InboxNotification.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-10-01.\n//\n\nimport Foundation\n\n@Observable\npublic class InboxNotification: ContentModel, ReadableProviding, Identifiable {\n    public var updateQueue: InboxNotificationUpdateQueue = .init()\n    \n    public var api: ApiClient\n\n    public let id: Int\n    // This can be removed when we drop support for < Lemmy 1.0\n    public let contentId: Int\n    \n    public var read: Bool\n    public let content: InboxNotificationContent\n\n    init(\n        api: ApiClient,\n        id: Int,\n        contentId: Int,\n        read: Bool,\n        content: InboxNotificationContent\n    ) {\n        self.api = api\n        self.id = id\n        self.contentId = contentId\n        self.read = read\n        self.content = content\n        \n        Task {\n            await updateQueue.setParent(self)\n        }\n    }\n    \n    public func updateRead(_ newValue: Bool) {\n        read = newValue\n        let type: InboxItemType = switch content.type {\n        case .mention: .mention\n        case .reply: .reply\n        case .message: .message\n        }\n\n        api.unreadCount?.updateUnverifiedItem(itemType: type, isRead: newValue)\n        Task {\n            await updateQueue.addItem {\n                try await self.api.repository.markNotificationAsRead(\n                    type: self.content.type,\n                    id: self.id,\n                    contentId: self.contentId,\n                    read: newValue\n                )\n                // TODO: unified InboxItem update this whole thing to be properties-based\n                guard var snapshot = self.takeSnapshot() else {\n                    assertionFailure(\"updateRead called on a notification that cannot take a snapshot\")\n                    throw ApiClientError.invalidInput\n                }\n                snapshot.read = newValue\n                self.api.unreadCount?.verifyItem(itemType: type, isRead: snapshot.read)\n                return snapshot\n            }\n        }\n    }\n    \n    public func toggleRead() {\n        updateRead(!read)\n    }\n\n    public static func == (lhs: InboxNotification, rhs: InboxNotification) -> Bool {\n        lhs.id == rhs.id\n    }\n}\n\nextension InboxNotification: InboxIdentifiable {\n    public var inboxId: Int { content.wrappedValue.actorId.hashValue }\n}\n\nextension InboxNotification: FeedLoadable {\n    public typealias FilterType = InboxItemFilterType\n\n    public func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        switch sortType {\n        case .new:\n            switch self.content {\n            case let .mention(comment), let .reply(comment):\n                return .new(comment.created)\n            case let .message(message):\n                return .new(message.created)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Notification/InboxNotificationContent.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-10-12.\n//\n\nimport Foundation\n\npublic enum InboxNotificationContent {\n    case reply(Comment)\n    case mention(Comment)\n    case message(Message2)\n    \n    public var wrappedValue: any ContentModel & ActorIdentifiable {\n        switch self {\n        case let .reply(comment): comment\n        case let .mention(comment): comment\n        case let .message(message2): message2\n        }\n    }\n    \n    public var type: InboxNotificationContentType {\n        switch self {\n        case .reply: .reply\n        case .mention: .mention\n        case .message: .message\n        }\n    }\n    \n    func takeSnapshot() -> InboxNotificationContentSnapshot? {\n        switch self {\n        case let .reply(comment):\n            if let snapshot = comment.takeSnapshot2() {\n                .reply(snapshot)\n            } else {\n                nil\n            }\n        case let .mention(comment):\n            if let snapshot = comment.takeSnapshot2() {\n                .mention(snapshot)\n            } else {\n                nil\n            }\n        case let .message(message): .message(message.takeSnapshot2())\n        }\n    }\n}\n\npublic enum InboxNotificationContentType: Hashable {\n    case reply, mention, message\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/OwnershipProviding.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-10-17.\n//\n\nimport Foundation\n\npublic protocol OwnershipProviding: ContentIdentifiable {\n    func isOwnContent(myPersonId: Int) -> Bool\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Person/Person+Conformance.swift",
    "content": "//\n//  Person+Conformance.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-26.\n//\n\nimport Foundation\n\n// MARK: CacheIdentifiable\n\npublic extension Person {\n    var cacheId: Int { id }\n}\n\n// MARK: SelectableContentProviding\n\npublic extension Person {\n    var selectableContent: String? { description }\n}\n\n// MARK: ProfileProviding\n\npublic extension Person {\n    var profileCreated: Date? { created }\n}\n\n// MARK: CommunityOrPerson\n\npublic extension Person {\n    static var identifierPrefix: String { \"@\" }\n}\n\n// MARK: ContentIdentifiable\n\npublic extension Person {\n    static var modelTypeId: ContentType { .person }\n}\n\n// MARK: Resolvable\n\npublic extension Person {\n    /// Returns a `URL` that can be resolved by another `ApiClient`.\n    func resolvableUrl(from instance: ContentModelUrlType) -> URL {\n        switch instance {\n        case .host: actorId.url\n        case .provider: .person(host: api.host, name: name)\n        }\n    }\n    \n    @inlinable\n    var allResolvableUrls: [URL] {\n        ContentModelUrlType.allCases.map { resolvableUrl(from: $0) }\n    }\n}\n\n// MARK: Sharable\n\npublic extension Person {\n    func url() -> URL {\n        if apiIsLocal {\n            api.baseUrl.appending(path: \"u/\\(name)\")\n        } else {\n            api.baseUrl.appending(path: \"u/\\(name)@\\(host)\")\n        }\n    }\n}\n\n// MARK: FeedLoadable\n\npublic extension Person {\n    typealias FilterType = PersonFilterType\n    \n    func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        switch sortType {\n        case .new:\n            return .new(created)\n        }\n    }\n}\n\n// MARK: Blockable\n\npublic extension Person {\n    var updateBlocked: ((Bool, ((Bool) -> Void)?) -> Void)? { self._updateBlocked }\n    \n    private func _updateBlocked(_ newValue: Bool, callback: ((Bool) -> Void)? = nil) {\n        let oldValue = blocked_.realizedValue\n        blocked_.set(newValue)\n        \n        Task {\n            await updateQueue.addItem {\n                do {\n                    let snapshot = try await self.api.repository.blockPerson(id: self.id, block: newValue)\n                    callback?(true)\n                    if newValue {\n                        self.api.blocks?.people[self.actorId] = self.id\n                    } else {\n                        self.api.blocks?.people.removeValue(forKey: self.actorId)\n                    }\n                    return await .init(api: self.api, snapshot: .person2(snapshot))\n                } catch {\n                    self.blocked_.set(oldValue)\n                    callback?(false)\n                    throw error\n                }\n            }\n        }\n    }\n}\n\n// MARK: Codable\n\npublic extension Person {\n    struct CodedData: Codable {\n        let apiUrl: URL\n        let apiMyPersonId: Int?\n        let apiPerson: LemmyPerson\n    }\n    \n    internal var apiPerson: LemmyPerson {\n        .init(\n            id: id,\n            name: name,\n            displayName: displayName == name ? nil : displayName,\n            avatar: avatar,\n            banned: bannedFromInstance,\n            published: created,\n            updated: updated,\n            actorId: actorId,\n            bio: description,\n            local: apiIsLocal,\n            banner: banner,\n            deleted: deleted,\n            matrixUserId: matrixUserId,\n            botAccount: isBot,\n            banExpires: instanceBan.expiryDate,\n            instanceId: instanceId,\n            publishedAt: created,\n            updatedAt: updated,\n            apId: actorId,\n            lastRefreshedAt: nil,\n            postCount: nil,\n            commentCount: nil\n        )\n    }\n    \n    func codedData() async throws -> CodedData {\n        try await .init(\n            apiUrl: api.baseUrl,\n            apiMyPersonId: api.myPersonId,\n            apiPerson: apiPerson\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Person/Person.swift",
    "content": "//\n//  Person.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-26.\n//\n\nimport Observation\nimport Foundation\n\n@Observable\npublic final class Person:\n    UnifiedModelProviding,\n    Blockable,\n    ContentIdentifiable,\n    SelectableContentProviding,\n    CommunityOrPerson,\n    Resolvable,\n    PurgableProviding,\n    Sharable,\n    FeedLoadable,\n    ProfileProviding {\n    public typealias Properties = PersonProperties\n    \n    public var api: ApiClient\n    private let properties: PersonProperties\n    @ObservationIgnored lazy var updateQueue: UnifiedUpdateQueue<Person> = .init(parent: self, properties: properties)\n    \n    // MARK: Custom Properties\n    // Mlem-specific properties that are not reflected in the API\n    \n    public var blocked: any RealizedValueProviding<Bool> { blocked_ }\n    public var blocked_: RealizedValue<Bool>\n    public var purged: Bool = false\n    \n    // Communities from which this person is *known* to be banned.\n    // If an ID is not in this set, its status is unknown.\n    //\n    // Don't make this public. Instead, use the `bannedFromCommunity` property of\n    // Post2/Comment2/Reply2. Accessing it from there guarantees that the ban\n    // status is known. Those properties access this set as a shared source-of-truth.\n    var knownCommunityBanStates: [Int: Bool] = .init()\n    \n    // MARK: API Properties\n    // Properties that are provided by the API\n    \n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let name: String\n    public let created: Date\n    public let instanceId: Int\n    public var displayName: String\n    public var avatar: URL?\n    public var note: String?\n    public var updated: Date?\n    public var matrixUserId: String?\n    public var isBot: Bool\n    public var instanceBan: InstanceBanType\n    public var deleted: Bool\n\n    public var description: String?\n    public var banner: URL?\n    \n    public var isAdmin: ExpectedValue<Bool>\n    public var postCount: ExpectedValue<Int>\n    public var commentCount: ExpectedValue<Int>\n    public var instance: ExpectedValue<Instance>\n    public var moderatedCommunities: ExpectedValue<[Community]>\n    \n    public var email: ExpectedValue<String?>\n    public var showNsfw: ExpectedValue<Bool>\n    public var theme: ExpectedValue<String>\n    public var defaultListingType: ExpectedValue<ListingType>\n    public var interfaceLanguage: ExpectedValue<String>\n    public var showAvatars: ExpectedValue<Bool>\n    public var sendNotificationsToEmail: ExpectedValue<Bool>\n    public var showScores: ExpectedValue<Bool>\n    public var showBotAccounts: ExpectedValue<Bool>\n    public var showReadPosts: ExpectedValue<Bool>\n    public var discussionLanguageIds: ExpectedValue<Set<Int>>\n    public var emailVerified: ExpectedValue<Bool>\n    public var acceptedApplication: ExpectedValue<Bool>\n    public var openLinksInNewTab: ExpectedValue<Bool?>\n    public var blurNsfw: ExpectedValue<Bool?>\n    public var autoExpandImages: ExpectedValue<Bool?>\n    public var infiniteScrollEnabled: ExpectedValue<Bool?>\n    public var postListingMode: ExpectedValue<PostFeedViewMode?>\n    public var totp2faEnabled: ExpectedValue<Bool?>\n    public var enableKeyboardNavigation: ExpectedValue<Bool?>\n    public var enableAnimatedImages: ExpectedValue<Bool?>\n    public var collapseBotComments: ExpectedValue<Bool?>\n    \n    public init(api: ApiClient, properties: PersonProperties) {\n        self.api = api\n        self.properties = properties\n        self.blocked_ = .init(api.blocks?.people.keys.contains(properties.actorId) ?? false)\n        \n        self.actorId = properties.actorId\n        self.id = properties.id\n        self.name = properties.name\n        self.created = properties.created\n        self.instanceId = properties.instanceId\n        self.displayName = properties.displayName\n        self.avatar = properties.avatar\n        self.note = properties.note\n        self.updated = properties.updated\n        self.matrixUserId = properties.matrixUserId\n        self.isBot = properties.isBot\n        self.instanceBan = properties.instanceBan\n        self.deleted = properties.deleted\n\n        // nil-coalesced because PieFed doesn't return these values for some requests.\n        self.description = properties.description ?? nil\n        self.banner = properties.banner ?? nil\n        \n        // because upgrade() is not available until all properties are initialized, first populate all properties\n        // with ExpectedValues that don't actually do anything, then reassign them properly at the end of the init\n        // this is somewhat cumbersome but avoids lazy vars, which are very awkward in Observables\n        self.isAdmin = dummyExpectedValue(properties.isAdmin)\n        self.postCount = dummyExpectedValue(properties.postCount)\n        self.commentCount = dummyExpectedValue(properties.commentCount)\n        self.instance = dummyExpectedValue(properties.instance)\n        self.moderatedCommunities = dummyExpectedValue(properties.moderatedCommunities)\n        \n        self.email = dummyExpectedValue(properties.email)\n        self.showNsfw = dummyExpectedValue(properties.showNsfw)\n        self.theme = dummyExpectedValue(properties.theme)\n        self.defaultListingType = dummyExpectedValue(properties.defaultListingType)\n        self.interfaceLanguage = dummyExpectedValue(properties.interfaceLanguage)\n        self.showAvatars = dummyExpectedValue(properties.showAvatars)\n        self.sendNotificationsToEmail = dummyExpectedValue(properties.sendNotificationsToEmail)\n        self.showScores = dummyExpectedValue(properties.showScores)\n        self.showBotAccounts = dummyExpectedValue(properties.showBotAccounts)\n        self.showReadPosts = dummyExpectedValue(properties.showReadPosts)\n        self.discussionLanguageIds = dummyExpectedValue(properties.discussionLanguageIds)\n        self.emailVerified = dummyExpectedValue(properties.emailVerified)\n        self.acceptedApplication = dummyExpectedValue(properties.acceptedApplication)\n        self.openLinksInNewTab = dummyExpectedValue(properties.openLinksInNewTab)\n        self.blurNsfw = dummyExpectedValue(properties.blurNsfw)\n        self.autoExpandImages = dummyExpectedValue(properties.autoExpandImages)\n        self.infiniteScrollEnabled = dummyExpectedValue(properties.infiniteScrollEnabled)\n        self.postListingMode = dummyExpectedValue(properties.postListingMode)\n        self.totp2faEnabled = dummyExpectedValue(properties.totp2faEnabled)\n        self.enableKeyboardNavigation = dummyExpectedValue(properties.enableKeyboardNavigation)\n        self.enableAnimatedImages = dummyExpectedValue(properties.enableAnimatedImages)\n        self.collapseBotComments = dummyExpectedValue(properties.collapseBotComments)\n        \n        func expectedValue<T>(_ value: T?) -> ExpectedValue<T> {\n            .init(\n                value: value,\n                provideValue: { try await self.upgrade() })\n        }\n        self.isAdmin = expectedValue(properties.isAdmin)\n        self.postCount = expectedValue(properties.postCount)\n        self.commentCount = expectedValue(properties.commentCount)\n        self.instance = expectedValue(properties.instance)\n        self.moderatedCommunities = expectedValue(properties.moderatedCommunities)\n        \n        self.email = expectedValue(properties.email)\n        self.showNsfw = expectedValue(properties.showNsfw)\n        self.theme = expectedValue(properties.theme)\n        self.defaultListingType = expectedValue(properties.defaultListingType)\n        self.interfaceLanguage = expectedValue(properties.interfaceLanguage)\n        self.showAvatars = expectedValue(properties.showAvatars)\n        self.sendNotificationsToEmail = expectedValue(properties.sendNotificationsToEmail)\n        self.showScores = expectedValue(properties.showScores)\n        self.showBotAccounts = expectedValue(properties.showBotAccounts)\n        self.showReadPosts = expectedValue(properties.showReadPosts)\n        self.discussionLanguageIds = expectedValue(properties.discussionLanguageIds)\n        self.emailVerified = expectedValue(properties.emailVerified)\n        self.acceptedApplication = expectedValue(properties.acceptedApplication)\n        self.openLinksInNewTab = expectedValue(properties.openLinksInNewTab)\n        self.blurNsfw = expectedValue(properties.blurNsfw)\n        self.autoExpandImages = expectedValue(properties.autoExpandImages)\n        self.infiniteScrollEnabled = expectedValue(properties.infiniteScrollEnabled)\n        self.postListingMode = expectedValue(properties.postListingMode)\n        self.totp2faEnabled = expectedValue(properties.totp2faEnabled)\n        self.enableKeyboardNavigation = expectedValue(properties.enableKeyboardNavigation)\n        self.enableAnimatedImages = expectedValue(properties.enableAnimatedImages)\n        self.collapseBotComments = expectedValue(properties.collapseBotComments)\n    }\n    \n    public func update(with properties: PersonProperties) {\n        setIfChanged(\\.displayName, properties.displayName)\n        setIfChanged(\\.avatar, properties.avatar)\n        setIfChanged(\\.note, properties.note)\n        setIfChanged(\\.updated, properties.updated)\n        setIfChanged(\\.matrixUserId, properties.matrixUserId)\n        setIfChanged(\\.isBot, properties.isBot)\n        setIfChanged(\\.instanceBan, properties.instanceBan)\n        setIfChanged(\\.deleted, properties.deleted)\n\n        if let description = properties.description {\n            setIfChanged(\\.description, description)\n        }\n        if let banner = properties.banner {\n            setIfChanged(\\.banner, banner)\n        }\n        \n        updateIfChanged(\\.isAdmin.value_, properties.isAdmin)\n        updateIfChanged(\\.postCount.value_, properties.postCount)\n        updateIfChanged(\\.commentCount.value_, properties.commentCount)\n        \n        setIfNil(\\.instance.value_, properties.instance)\n        updateIfChanged(\\.moderatedCommunities.value_, properties.moderatedCommunities)\n        \n        updateIfChanged(\\.email.value_, properties.email)\n        updateIfChanged(\\.showNsfw.value_, properties.showNsfw)\n        updateIfChanged(\\.theme.value_, properties.theme)\n        updateIfChanged(\\.defaultListingType.value_, properties.defaultListingType)\n        updateIfChanged(\\.interfaceLanguage.value_, properties.interfaceLanguage)\n        updateIfChanged(\\.showAvatars.value_, properties.showAvatars)\n        updateIfChanged(\\.sendNotificationsToEmail.value_, properties.sendNotificationsToEmail)\n        updateIfChanged(\\.showScores.value_, properties.showScores)\n        updateIfChanged(\\.showBotAccounts.value_, properties.showBotAccounts)\n        updateIfChanged(\\.showReadPosts.value_, properties.showReadPosts)\n        updateIfChanged(\\.discussionLanguageIds.value_, properties.discussionLanguageIds)\n        updateIfChanged(\\.emailVerified.value_, properties.emailVerified)\n        updateIfChanged(\\.acceptedApplication.value_, properties.acceptedApplication)\n        updateIfChanged(\\.openLinksInNewTab.value_, properties.openLinksInNewTab)\n        updateIfChanged(\\.blurNsfw.value_, properties.blurNsfw)\n        updateIfChanged(\\.autoExpandImages.value_, properties.autoExpandImages)\n        updateIfChanged(\\.infiniteScrollEnabled.value_, properties.infiniteScrollEnabled)\n        updateIfChanged(\\.postListingMode.value_, properties.postListingMode)\n        updateIfChanged(\\.totp2faEnabled.value_, properties.totp2faEnabled)\n        updateIfChanged(\\.enableKeyboardNavigation.value_, properties.enableKeyboardNavigation)\n        updateIfChanged(\\.enableAnimatedImages.value_, properties.enableAnimatedImages)\n        updateIfChanged(\\.collapseBotComments.value_, properties.collapseBotComments)\n    }\n    \n    public func softUpdate(with properties: PersonProperties) {\n        setIfNil(\\.isAdmin.value_, properties.isAdmin)\n        setIfNil(\\.postCount.value_, properties.postCount)\n        setIfNil(\\.commentCount.value_, properties.commentCount)\n        \n        setIfNil(\\.instance.value_, properties.instance)\n        setIfNil(\\.moderatedCommunities.value_, properties.moderatedCommunities)\n    }\n    \n    // MARK: Upgrades\n    \n    public func upgrade() async throws {\n        try await updateQueue.upgrade()\n    }\n    \n    public func fetchUpgraded() async throws -> PersonProperties {\n        let snapshot = try await api.repository.getPerson(id: id)\n        return await .init(api: api, snapshot: .person3(snapshot))\n    }\n    \n    public func resolve(with api: ApiClient) async throws -> Self {\n        let stub = PersonStub(api: api, url: allResolvableUrls[0])\n        return try await stub.getPerson() as! Self\n    }\n    \n    // MARK: Logic\n    \n    func updateKnownCommunityBanState(id: Int, banned: Bool) {\n        if banned {\n            // This `if` statement avoids unneccessary state update\n            if !(knownCommunityBanStates[id] ?? false) {\n                knownCommunityBanStates[id] = true\n            }\n        } else {\n            if knownCommunityBanStates[id] ?? true {\n                knownCommunityBanStates[id] = false\n            }\n        }\n    }\n}\n\n// MARK: - Computed\n\npublic extension Person {\n    var bannedFromInstance: Bool { instanceBan != .notBanned }\n    \n    func isBannedFromCommunity(id: Int) -> Bool? {\n        knownCommunityBanStates[id]\n    }\n    \n    func isBannedFromCommunity(_ community: Community) -> Bool? {\n        isBannedFromCommunity(id: community.id)\n    }\n    \n    func profileDetails() -> ProfileDetails {\n        .init(\n            avatar: avatar,\n            banner: banner,\n            displayName: displayName,\n            description: description,\n            matrixUserId: matrixUserId\n        )\n    }\n    \n    var moderatedCommunityActorIds: Set<ActorIdentifier>? {\n        if let moderatedCommunities = moderatedCommunities.value {\n            .init(moderatedCommunities.map(\\.actorId))\n        } else {\n            nil\n        }\n    }\n    \n    var moderates: ((CommunityIdentifier) -> Bool)? {\n        if let moderatedCommunities = moderatedCommunities.value {\n            return { communityIdentifier in\n                switch communityIdentifier {\n                case let .id(id): moderatedCommunities.contains { $0.id == id }\n                case let .actorId(actorId): moderatedCommunities.contains { $0.actorId == actorId }\n                case let .community(community): moderatedCommunities.contains { $0.actorId == community.actorId }\n                }\n            }\n        }\n        return nil\n    }\n    \n    /// Returns true if this person can perform moderator actions on the target person\n    func canModerate(_ person: Person, communityModerators: [Person]) -> Bool {\n        // admins can moderate anybody but a higher-ranking admin\n        if isAdmin.value ?? false {\n            if person.isAdmin.value ?? false {\n                return api.isHigherAdmin(than: person)\n            }\n            return true\n        }\n        \n        // if this person is not a mod, can't moderate\n        guard let myModIndex = communityModerators.firstIndex(where: { $0.id == id }) else {\n            return false\n        }\n        \n        // if target is a mod, check that this person outranks them\n        if let targetModIndex = communityModerators.firstIndex(where: { $0.id == person.id }) {\n            return myModIndex < targetModIndex\n        }\n        \n        // if target not a mod, can moderate\n        return true\n    }\n}\n\n// MARK: - Interactions\n\npublic extension Person {\n    \n    // Get Content\n    \n    func getContent(\n        community: Community? = nil,\n        sort: PostSortType = .new,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool = false\n    ) async throws -> (person: Person, posts: [Post], comments: [Comment]) {\n        try await api.getContent(\n            authorId: id,\n            sort: sort,\n            page: page,\n            limit: limit,\n            savedOnly: savedOnly,\n            communityId: community?.id\n        )\n    }\n    \n    // MARK: Ban\n    \n    func ban(from community: Community, removeContent: Bool, reason: String?, expires: Date?) async throws {\n        try await api.banPersonFromCommunity(\n            personId: id,\n            communityId: community.id,\n            ban: true,\n            removeContent: removeContent,\n            reason: reason,\n            expires: expires\n        )\n    }\n    \n    func unban(from community: Community, reason: String?) async throws {\n        try await api.banPersonFromCommunity(\n            personId: id,\n            communityId: community.id,\n            ban: false,\n            removeContent: false,\n            reason: reason\n        )\n    }\n    \n    // MARK: Purge\n    \n    func purge(reason: String?) async throws {\n        try await api.purgePerson(id: id, reason: reason)\n    }\n    \n    func banFromInstance(removeContent: Bool, reason: String?, expires: Date?) async throws {\n        try await api.banPersonFromInstance(\n            personId: id,\n            ban: true,\n            removeContent: removeContent,\n            reason: reason,\n            expires: expires\n        )\n    }\n    \n    func unbanFromInstance(reason: String?) async throws {\n        try await api.banPersonFromInstance(\n            personId: id,\n            ban: false,\n            removeContent: false,\n            reason: reason,\n            expires: nil\n        )\n    }\n    \n    // Note\n    \n    func updateNote(content: String?) {\n        note = content\n        \n        Task {\n            await updateQueue.addItem { properties in\n                var properties = properties\n                try await self.api.repository.editNote(id: self.id, content: content)\n                properties.note = content\n                return properties\n            }\n        }\n    }\n    \n    // Profile\n    \n    // TODO: NOW make a User concept?\n    \n    func updateProfile(_ details: ProfileDetails) async throws {\n        let diff = ProfileDetailsMutation(\n            originalDetails: profileDetails(),\n            newDetails: details\n        )\n        if try await !(diff.isValid(forSoftware: api.software)) {\n            throw ApiClientError.invalidInput\n        }\n        \n        avatar = details.avatar\n        banner = details.banner\n        displayName = details.displayName ?? displayName\n        description = details.description\n        matrixUserId = details.matrixUserId\n        \n        await updateQueue.addItem { properties in\n            try await self.api.editProfile(details)\n            \n            var properties = properties\n            properties.avatar = details.avatar\n            properties.banner = details.banner\n            properties.displayName = details.displayName ?? properties.displayName\n            properties.description = details.description\n            properties.matrixUserId = details.matrixUserId\n            return properties\n        }\n    }\n    \n    struct ProfileSettings {\n        let email: String?\n        let matrixUserId: String?\n        let showNsfw: Bool?\n        let blurNsfw: Bool?\n        let showBotAccounts: Bool?\n        let discussionLanguageIds: Set<Int>?\n        let sendNotificationsToEmail: Bool?\n        let isBot: Bool?\n        \n        public init(\n            email: String? = nil,\n            matrixUserId: String? = nil,\n            showNsfw: Bool? = nil,\n            blurNsfw: Bool? = nil,\n            showBotAccounts: Bool? = nil,\n            discussionLanguageIds: Set<Int>? = nil,\n            sendNotificationsToEmail: Bool? = nil,\n            isBot: Bool? = nil,\n        ) {\n            self.email = email\n            self.matrixUserId = matrixUserId\n            self.showNsfw = showNsfw\n            self.blurNsfw = blurNsfw\n            self.showBotAccounts = showBotAccounts\n            self.discussionLanguageIds = discussionLanguageIds\n            self.sendNotificationsToEmail = sendNotificationsToEmail\n            self.isBot = isBot\n        }\n    }\n    \n    var updateSettings: ((ProfileSettings) async throws -> Void)? {\n        if let showNsfw = self.showNsfw.value,\n           let showScores = self.showScores.value,\n           let theme = self.theme.value,\n           let defaultListingType = self.defaultListingType.value,\n           let interfaceLanguage = self.interfaceLanguage.value,\n           let email = self.email.value,\n           let showAvatars = self.showAvatars.value,\n           let sendNotificationsToEmail = self.sendNotificationsToEmail.value,\n           let showBotAccounts = self.showBotAccounts.value,\n           let showReadPosts = self.showReadPosts.value,\n           let discussionLanguages = self.discussionLanguageIds.value,\n           let openLinksInNewTab = self.openLinksInNewTab.value,\n           let blurNsfw = self.blurNsfw.value,\n           let autoExpandImages = self.autoExpandImages.value,\n           let infiniteScrollEnabled = self.infiniteScrollEnabled.value,\n           let postListingMode = self.postListingMode.value,\n           let enableKeyboardNavigation = self.enableKeyboardNavigation.value,\n           let enableAnimatedImages = self.enableAnimatedImages.value,\n           let collapseBotComments = self.collapseBotComments.value {\n            return { profileSettings in\n                await self.updateQueue.addItem { properties in\n                    // this function has some untidy source-of-truth behavior--canonically we want to use the provided properties from the UpdateQueue,\n                    // but those are not guaranteed to have user-tier fields so we fall back on the guaranteed values from the `if let` wall above.\n                    // note also that a `nil` in `ProfileSettings` indicates no change\n                    let newEmail: String? = profileSettings.email ?? properties.email ?? email\n                    let newMatrixUserId: String? = profileSettings.matrixUserId ?? properties.matrixUserId\n                    let newShowNsfw: Bool = profileSettings.showNsfw ?? properties.showNsfw ?? showNsfw\n                    let newShowBotAccounts: Bool = profileSettings.showBotAccounts ?? properties.showBotAccounts ?? showBotAccounts\n                    let newDiscussionLanguageIds: Set<Int> = (profileSettings.discussionLanguageIds ?? properties.discussionLanguageIds ?? discussionLanguages)\n                    let newSendNotificationsToEmail: Bool = profileSettings.sendNotificationsToEmail ?? properties.sendNotificationsToEmail ?? sendNotificationsToEmail\n                    let newIsBot: Bool = profileSettings.isBot ?? properties.isBot\n                    \n                    try await self.api.editAccountSettings(\n                        showNsfw: newShowNsfw,\n                        showScores: properties.showScores ?? showScores,\n                        theme: properties.theme ?? theme,\n                        defaultListingType: properties.defaultListingType ?? defaultListingType,\n                        interfaceLanguage: properties.interfaceLanguage ?? interfaceLanguage,\n                        avatar: properties.avatar?.absoluteString ?? \"\",\n                        banner: properties.banner??.absoluteString ?? \"\",\n                        displayName: properties.displayName,\n                        email: newEmail,\n                        bio: properties.description ?? \"\",\n                        matrixUserId: newMatrixUserId,\n                        showAvatars: properties.showAvatars ?? showAvatars,\n                        sendNotificationsToEmail: newSendNotificationsToEmail,\n                        botAccount: newIsBot,\n                        showBotAccounts: newShowBotAccounts,\n                        showReadPosts: properties.showReadPosts ?? showReadPosts,\n                        discussionLanguages: newDiscussionLanguageIds.sorted(),\n                        openLinksInNewTab: properties.openLinksInNewTab ?? openLinksInNewTab,\n                        blurNsfw: profileSettings.blurNsfw ?? (properties.blurNsfw as? Bool) ?? blurNsfw,\n                        autoExpand: properties.autoExpandImages ?? autoExpandImages,\n                        infiniteScrollEnabled: properties.infiniteScrollEnabled ?? infiniteScrollEnabled,\n                        postListingMode: properties.postListingMode ?? postListingMode,\n                        enableKeyboardNavigation: properties.enableKeyboardNavigation ?? enableKeyboardNavigation,\n                        enableAnimatedImages: properties.enableAnimatedImages ?? enableAnimatedImages,\n                        collapseBotComments: properties.collapseBotComments ?? collapseBotComments,\n                        showUpvotes: nil,\n                        showDownvotes: nil,\n                        showUpvotePercentage: nil\n                    )\n                    \n                    var properties = properties\n                    properties.email = newEmail\n                    properties.matrixUserId = newMatrixUserId\n                    properties.showNsfw = newShowNsfw\n                    properties.showBotAccounts = newShowBotAccounts\n                    properties.discussionLanguageIds = newDiscussionLanguageIds\n                    properties.sendNotificationsToEmail = newSendNotificationsToEmail\n                    properties.isBot = newIsBot\n                    return properties\n                }\n            }\n        }\n        return nil\n    }\n}\n\npublic enum CommunityIdentifier {\n    case id(Int)\n    case actorId(ActorIdentifier)\n    case community(Community)\n}\n\n// MARK: Shim\n\npublic extension Person {\n    func takeSnapshot1() -> Person1Snapshot {\n        return .init(\n            actorId: actorId,\n            id: id,\n            name: name,\n            created: created,\n            instanceId: instanceId,\n            displayName: displayName,\n            avatar: avatar,\n            banner: banner,\n            note: note,\n            updated: updated,\n            description: description,\n            matrixUserId: matrixUserId,\n            isBot: isBot,\n            instanceBan: instanceBan,\n            deleted: deleted,\n            allPropertiesPresent: true\n        )\n    }\n}\n\npublic extension Person {\n    var displayName_: String? { displayName }\n    var description_: String? { description }\n    var banner_: URL? { banner }\n    var created_: Date? { created }\n    var updated_: Date? { updated }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Person/Person1+Mock.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-02.\n//\n\nimport Foundation\n\n// TODO: updated mocks\n//#if DEBUG\n//    public extension Person1 {\n//        static func mock(\n//            api: MockApiClient = .mock,\n//            actorId: ActorIdentifier?,\n//            id: Int,\n//            name: String,\n//            created: Date,\n//            instanceId: Int,\n//            updated: Date?,\n//            displayName: String,\n//            description: String?,\n//            matrixUserId: String?,\n//            avatar: URL?,\n//            banner: URL?,\n//            deleted: Bool,\n//            isBot: Bool,\n//            instanceBan: InstanceBanType,\n//            blocked: Bool\n//        ) -> Person1 {\n//            .init(\n//                api: api,\n//                actorId: actorId ?? .init(url: URL(string: \"https://\\(api.host)/u/\\(name)\")!)!,\n//                id: id,\n//                name: name,\n//                created: created,\n//                instanceId: instanceId,\n//                updated: updated,\n//                displayName: displayName,\n//                description: description,\n//                matrixUserId: matrixUserId,\n//                avatar: avatar,\n//                banner: banner,\n//                note: nil,\n//                deleted: deleted,\n//                isBot: isBot,\n//                instanceBan: instanceBan,\n//                blocked: blocked\n//            )\n//        }\n//    }\n//#endif\n\n//#if DEBUG\n//    public extension Person2 {\n//        static func mock(\n//            person1: Person1,\n//            postCount: Int,\n//            commentCount: Int,\n//            isAdmin: Bool\n//        ) -> Person2 {\n//            Person2(\n//                api: person1.api,\n//                person1: person1,\n//                postCount: postCount,\n//                commentCount: commentCount,\n//                isAdmin: isAdmin\n//            )\n//        }\n//    }\n//#endif\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Person/PersonProperties.swift",
    "content": "//\n//  PersonProperties.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-26.\n//\n\nimport Foundation\n\npublic struct PersonProperties: UnifiedPropertiesProviding {\n    // From Person1Snapshot, guaranteed to always be present\n    let actorId: ActorIdentifier\n    let id: Int\n    let name: String\n    let created: Date\n    let instanceId: Int\n    var displayName: String\n    var avatar: URL?\n    var note: String?\n    var updated: Date?\n    var matrixUserId: String?\n    var isBot: Bool\n    var instanceBan: InstanceBanType\n    var deleted: Bool\n\n    // From Person1Snapshot, but PieFed does not always provide these\n    // https://codeberg.org/rimu/pyfedi/issues/882\n    var description: String??\n    var banner: URL??\n    \n    // From Person2Snapshot\n    var isAdmin: Bool?\n    var postCount: Int?\n    var commentCount: Int?\n    \n    // From Person3Snapshot\n    var instance: Instance?\n    var moderatedCommunities: [Community]?\n    \n    // From Person4Snapshot\n    var email: String??\n    var showNsfw: Bool?\n    var theme: String?\n    var defaultListingType: ListingType?\n    var interfaceLanguage: String?\n    var showAvatars: Bool?\n    var sendNotificationsToEmail: Bool?\n    var showScores: Bool?\n    var showBotAccounts: Bool?\n    var showReadPosts: Bool?\n    var discussionLanguageIds: Set<Int>?\n    var emailVerified: Bool?\n    var acceptedApplication: Bool?\n    var openLinksInNewTab: Bool??\n    var blurNsfw: Bool??\n    var autoExpandImages: Bool??\n    var infiniteScrollEnabled: Bool??\n    var postListingMode: PostFeedViewMode??\n    var totp2faEnabled: Bool??\n    var enableKeyboardNavigation: Bool??\n    var enableAnimatedImages: Bool??\n    var collapseBotComments: Bool??\n    \n    @MainActor\n    public init(api: ApiClient, snapshot: AnyPersonSnapshot) {\n        let snapshot1: Person1Snapshot\n        let snapshot2: Person2Snapshot?\n        let snapshot3: Person3Snapshot?\n        let snapshot4: Person4Snapshot?\n        switch snapshot {\n        case let .person1(person1Snapshot):\n            snapshot1 = person1Snapshot\n            snapshot2 = nil\n            snapshot3 = nil\n            snapshot4 = nil\n        case let .person2(person2Snapshot):\n            snapshot1 = person2Snapshot.person\n            snapshot2 = person2Snapshot\n            snapshot3 = nil\n            snapshot4 = nil\n        case let .person3(person3Snapshot):\n            snapshot1 = person3Snapshot.person.person\n            snapshot2 = person3Snapshot.person\n            snapshot3 = person3Snapshot\n            snapshot4 = nil\n        case let .person4(person4Snapshot):\n            snapshot1 = person4Snapshot.person.person.person\n            snapshot2 = person4Snapshot.person.person\n            snapshot3 = person4Snapshot.person\n            snapshot4 = person4Snapshot\n        }\n        \n        if let snapshot4 {\n            email = snapshot4.email\n            showNsfw = snapshot4.showNsfw\n            theme = snapshot4.theme\n            defaultListingType = snapshot4.defaultListingType\n            interfaceLanguage = snapshot4.interfaceLanguage\n            showAvatars = snapshot4.showAvatars\n            sendNotificationsToEmail = snapshot4.sendNotificationsToEmail\n            showScores = snapshot4.showScores\n            showBotAccounts = snapshot4.showBotAccounts\n            showReadPosts = snapshot4.showReadPosts\n            discussionLanguageIds = snapshot4.discussionLanguageIds\n            emailVerified = snapshot4.emailVerified\n            acceptedApplication = snapshot4.acceptedApplication\n            openLinksInNewTab = snapshot4.openLinksInNewTab\n            blurNsfw = snapshot4.blurNsfw\n            autoExpandImages = snapshot4.autoExpandImages\n            infiniteScrollEnabled = snapshot4.infiniteScrollEnabled\n            postListingMode = snapshot4.postListingMode\n            totp2faEnabled = snapshot4.totp2faEnabled\n            enableKeyboardNavigation = snapshot4.enableKeyboardNavigation\n            enableAnimatedImages = snapshot4.enableAnimatedImages\n            collapseBotComments = snapshot4.collapseBotComments\n        }\n        \n        if let snapshot3 {\n            if let instance1Snapshot = snapshot3.site {\n                instance = api.caches.instance.getModel(api: api, from: .instance1(instance1Snapshot))\n            }\n            moderatedCommunities = api.caches.community.getModels(api: api, from: snapshot3.moderatedCommunities.map { .community1($0) })\n        }\n        \n        if let snapshot2 {\n            isAdmin = snapshot2.isAdmin\n            postCount = snapshot2.postCount\n            commentCount = snapshot2.commentCount\n        }\n        \n        actorId = snapshot1.actorId\n        id = snapshot1.id\n        name = snapshot1.name\n        created = snapshot1.created\n        instanceId = snapshot1.instanceId\n        displayName = snapshot1.displayName\n        avatar = snapshot1.avatar\n        note = snapshot1.note\n        updated = snapshot1.updated\n        matrixUserId = snapshot1.matrixUserId\n        isBot = snapshot1.isBot\n        instanceBan = snapshot1.instanceBan\n        deleted = snapshot1.deleted\n\n        if snapshot1.allPropertiesPresent {\n            description = snapshot1.description\n            banner = snapshot1.banner\n        }\n    }\n    \n    public mutating func merge(_ other: PersonProperties) {\n        // tier 1 properties: simple assignment\n        self.displayName = other.displayName\n        self.avatar = other.avatar\n        self.note = other.note\n        self.updated = other.updated\n        self.matrixUserId = other.matrixUserId\n        self.isBot = other.isBot\n        self.instanceBan = other.instanceBan\n        self.deleted = other.deleted\n        \n        // tier 2, 3, 4 properties: only assign if incoming non-nil\n        self.description = other.description ?? self.description\n        self.banner = other.banner ?? self.banner\n\n        isAdmin = other.isAdmin ?? self.isAdmin\n        postCount = other.postCount ?? self.postCount\n        commentCount = other.commentCount ?? self.commentCount\n\n        instance = other.instance ?? self.instance\n        moderatedCommunities = other.moderatedCommunities ?? self.moderatedCommunities\n        \n        email = other.email ?? self.email\n        showNsfw = other.showNsfw ?? self.showNsfw\n        theme = other.theme ?? self.theme\n        defaultListingType = other.defaultListingType ?? self.defaultListingType\n        interfaceLanguage = other.interfaceLanguage ?? self.interfaceLanguage\n        showAvatars = other.showAvatars ?? self.showAvatars\n        sendNotificationsToEmail = other.sendNotificationsToEmail ?? self.sendNotificationsToEmail\n        showScores = other.showScores ?? self.showScores\n        showBotAccounts = other.showBotAccounts ?? self.showBotAccounts\n        showReadPosts = other.showReadPosts ?? self.showReadPosts\n        discussionLanguageIds = other.discussionLanguageIds ?? self.discussionLanguageIds\n        emailVerified = other.emailVerified ?? self.emailVerified\n        acceptedApplication = other.acceptedApplication ?? self.acceptedApplication\n        openLinksInNewTab = other.openLinksInNewTab ?? self.openLinksInNewTab\n        blurNsfw = other.blurNsfw ?? self.blurNsfw\n        autoExpandImages = other.autoExpandImages ?? self.autoExpandImages\n        infiniteScrollEnabled = other.infiniteScrollEnabled ?? self.infiniteScrollEnabled\n        postListingMode = other.postListingMode ?? self.postListingMode\n        totp2faEnabled = other.totp2faEnabled ?? self.totp2faEnabled\n        enableKeyboardNavigation = other.enableKeyboardNavigation ?? self.enableKeyboardNavigation\n        enableAnimatedImages = other.enableAnimatedImages ?? self.enableAnimatedImages\n        collapseBotComments = other.collapseBotComments ?? self.collapseBotComments\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Person/PersonStub.swift",
    "content": "//\n//  Account.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/02/2024.\n//\n\nimport Foundation\nimport Observation\n\npublic struct PersonStub: Hashable {\n    public var api: ApiClient\n    public let url: URL\n    \n    public init(api: ApiClient, url: URL) {\n        self.api = api\n        self.url = url\n    }\n    \n    public func asLocal() -> Self {\n        .init(api: .getApiClient(url: url, username: nil), url: url)\n    }\n    \n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(url)\n    }\n    \n    public static func == (lhs: PersonStub, rhs: PersonStub) -> Bool {\n        lhs.url == rhs.url\n    }\n    \n    public func getPerson() async throws -> Person {\n        try await api.getPerson(url: url)\n    }\n}\n\n// Resolvable conformance\npublic extension PersonStub {\n    var resolvableUrl: URL { url }\n    \n    @inlinable\n    var allResolvableUrls: [URL] { [resolvableUrl] }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PersonVote/PersonVote+CacheExtensions.swift",
    "content": "//\n//  PersonVote+CacheExtensions.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-18.\n//\n\nimport Foundation\n\nextension PersonVote: CacheIdentifiable {\n    public var cacheId: Int {\n        var hasher = Hasher()\n        hasher.combine(target)\n        hasher.combine(creator.id)\n        return hasher.finalize()\n    }\n    \n    @MainActor\n    func update(with snapshot: PersonVoteSnapshot, semaphore: UInt? = nil) {\n        setIfChanged(\\.vote, ScoringOperation(rawValue: snapshot.score) ?? .none)\n        if let creatorBannedFromCommunity = snapshot.creatorBannedFromCommunity {\n            creator.updateKnownCommunityBanState(id: communityId, banned: creatorBannedFromCommunity)\n        }\n        Task {\n            await creator.updateQueue.attemptDirectUpdate(with: .init(api: api, snapshot: .person1(snapshot.creator)))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PersonVote/PersonVote.swift",
    "content": "//\n//  PersonVote.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-18.\n//\n\nimport Foundation\nimport Observation\n\n@Observable\npublic class PersonVote: ContentModel {\n    \n    public enum Target: Hashable {\n        case post(id: Int)\n        case comment(id: Int)\n    }\n    \n    public let api: ApiClient\n\n    public let target: Target\n    public let communityId: Int\n    \n    public let creator: Person\n    public var vote: ScoringOperation\n    \n    public var bannedFromCommunity: Bool {\n        guard let state = creator.isBannedFromCommunity(id: communityId) else {\n            assertionFailure(\"Ban status should be present at this point\")\n            return false\n        }\n        return state\n    }\n\n    init(\n        api: ApiClient,\n        target: Target,\n        communityId: Int,\n        creator: Person,\n        vote: ScoringOperation,\n        creatorBannedFromCommunity: Bool?\n    ) {\n        self.api = api\n        self.target = target\n        self.communityId = communityId\n        self.creator = creator\n        self.vote = vote\n        if let creatorBannedFromCommunity {\n            creator.updateKnownCommunityBanState(id: communityId, banned: creatorBannedFromCommunity)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PersonalUnreadCountSnapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic struct PersonalUnreadCountSnapshot {\n    let replies: Int\n    let mentions: Int\n    let messages: Int\n    \n    var unreadCountDictionary: [InboxItemType: Int] {\n        [\n            .reply: replies,\n            .mention: mentions,\n            .message: messages\n        ]\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/BlockListSnapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-25.\n//\n\nimport Foundation\n\npublic extension BlockListSnapshot {\n    init(from myUserInfo: PieFedMyUserInfo) {\n        self.people = myUserInfo.personBlocks.reduce(into: [:]) {\n            $0[$1.target.actorId] = $1.target.id\n        }\n        \n        self.communities = myUserInfo.communityBlocks.reduce(into: [:]) {\n            $0[$1.community.actorId] = $1.community.id\n        }\n        \n        self.instances = myUserInfo.instanceBlocks.reduce(into: [:]) {\n            let actorId: ActorIdentifier = .instance(host: $1.instance.domain)\n            $0[actorId] = actorId.url.absoluteString.hashValue\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Comment1Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Comment1Snapshot {\n    init(from comment: PieFedComment) throws(ApiClientError) {\n        let parentCommentIds = comment.path\n            .split(separator: \".\")\n            .dropFirst()\n            .dropLast()\n            .compactMap { Int($0) }\n\n        self.init(\n            actorId: comment.apId,\n            id: comment.id,\n            creatorId: comment.userId,\n            postId: comment.postId,\n            parentCommentIds: parentCommentIds,\n            created: comment.published,\n            content: comment.body,\n            updated: comment.updated,\n            distinguished: comment.distinguished ?? false,\n            languageId: comment.languageId,\n            // If a post is removed, deleted is true for some reason\n            deleted: comment.removed ? false : comment.deleted,\n            removed: comment.removed\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Comment2Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Comment2Snapshot {\n    init(from comment: PieFedCommentView) throws(ApiClientError) {\n        let votes: VotesModel = .init(\n            upvotes: comment.counts.upvotes,\n            downvotes: comment.counts.downvotes,\n            myVote: .guaranteedInit(from: comment.myVote)\n        )\n\n        try self.init(\n            comment: .init(from: comment.comment),\n            creator: .init(from: comment.creator),\n            post: .init(from: comment.post),\n            community: .init(from: comment.community),\n            commentCount: comment.counts.childCount,\n            creatorIsModerator: comment.creatorIsModerator,\n            creatorIsAdmin: comment.creatorIsAdmin,\n            creatorBannedFromCommunity: comment.creatorBannedFromCommunity,\n            votes: votes,\n            saved: comment.saved\n        )\n    }\n    \n    init(from report: PieFedCommentReportView) throws(ApiClientError) {\n        let votes: VotesModel = .init(\n            from: report.counts,\n            myVote: .guaranteedInit(from: report.myVote)\n        )\n\n        try self.init(\n            comment: .init(from: report.comment),\n            creator: .init(from: report.commentCreator),\n            post: .init(from: report.post),\n            community: .init(from: report.community),\n            commentCount: report.counts.childCount,\n            creatorIsModerator: report.creatorIsModerator,\n            creatorIsAdmin: report.creatorIsAdmin,\n            creatorBannedFromCommunity: report.creatorBannedFromCommunity,\n            votes: votes,\n            saved: report.saved\n        )\n    }\n\n    init(from reply: PieFedCommentReplyView) throws(ApiClientError) {\n        let votes: VotesModel = .init(\n            from: reply.counts,\n            myVote: .guaranteedInit(from: reply.myVote)\n        )\n\n        try self.init(\n            comment: .init(from: reply.comment),\n            creator: .init(from: reply.creator),\n            post: .init(from: reply.post),\n            community: .init(from: reply.community),\n            commentCount: reply.counts.childCount,\n            creatorIsModerator: reply.creatorIsModerator,\n            creatorIsAdmin: reply.creatorIsAdmin,\n            creatorBannedFromCommunity: reply.creatorBannedFromCommunity,\n            votes: votes,\n            saved: reply.saved\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/CommentSortType+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension CommentSortType {\n    var piefedSortType: PieFedSortType? {\n        switch self {\n        case .new: .new\n        case .old: .old\n        case .hot: .hot\n        case .controversial: nil\n        case .top(.allTime): .top\n        case .top: nil\n        }\n    }\n\n    var piefedCommentSortType: PieFedCommentSortType? {\n        switch self {\n        case .new: .new\n        case .old: .old\n        case .hot: .hot\n        case .controversial: .controversial\n        case .top(.allTime): .top\n        case .top: nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Community1Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Community1Snapshot {\n    init(from community: PieFedCommunity, allPropertiesPresent: Bool = false) throws(ApiClientError) {\n        self.init(\n            actorId: community.actorId,\n            id: community.id,\n            name: community.name,\n            created: community.published,\n            instanceId: community.instanceId,\n            updated: community.updated,\n            displayName: community.title,\n            description: community.description,\n            deleted: community.deleted,\n            removed: community.removed,\n            nsfw: community.nsfw,\n            avatar: community.icon,\n            banner: community.banner,\n            hidden: community.hidden,\n            onlyModeratorsCanPost: community.restrictedToMods,\n            allPropertiesPresent: allPropertiesPresent\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Community2Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Community2Snapshot {\n    init(from community: PieFedCommunityView, allPropertiesPresent: Bool = false) throws(ApiClientError) {\n        let subscription: SubscriptionModel = .init(\n            total: community.counts.totalSubscriptionsCount,\n            local: community.counts.subscriptionsCount,\n            subscribed: community.subscribed.isSubscribed,\n            pending: community.subscribed == .pending\n        )\n        \n        let activeUserCount: ActiveUserCount = .init(\n            sixMonths: community.counts.active6monthly ?? 0,\n            month: community.counts.activeMonthly ?? 0,\n            week: community.counts.activeWeekly ?? 0,\n            day: community.counts.activeDaily ?? 0\n        )\n            \n        try self.init(\n            community: .init(from: community.community, allPropertiesPresent: allPropertiesPresent),\n            subscription: subscription,\n            postCount: community.counts.postCount,\n            commentCount: community.counts.postReplyCount,\n            activeUserCount: activeUserCount,\n            bannedFromCommunity: false\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Community3Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Community3Snapshot {\n    init(from community: PieFedGetCommunityResponse) throws(ApiClientError) {\n        var moderators = [Person1Snapshot]()\n        for moderator in community.moderators {\n            try moderators.append(.init(from: moderator.moderator))\n        }\n\n        self.init(\n            community: try .init(from: community.communityView, allPropertiesPresent: true),\n            instance: try community.site.map {site throws(ApiClientError) in\n                try .init(from: site)\n            },\n            moderators: moderators,\n            discussionLanguageIds: .init(community.discussionLanguages)\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/ImageUpload1Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-05.\n//\n\nimport Foundation\n\npublic extension ImageUpload1Snapshot {\n    init(from response: PieFedImageUploadResponse) {\n        self.init(\n            url: response.url,\n            alias: nil,\n            deleteToken: nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/InboxNotificationSnapshot+PieFed.swift",
    "content": "//\n//  InboxNotificationSnapshot+PieFed.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-12-22.\n//\n\nimport Foundation\n\nextension InboxNotificationSnapshot {\n    init(from replyView: PieFedCommentReplyView, isMention: Bool) throws(ApiClientError) {\n        try self.init(\n            id: LegacyNotificationIdWrapper(type: isMention ? .mention : .reply, id: replyView.commentReply.id).hashValue,\n            contentId: replyView.commentReply.id,\n            read: replyView.commentReply.read,\n            content: .reply(.init(from: replyView))\n        )\n    }\n\n    init(from messageView: PieFedPrivateMessageView) throws(ApiClientError) {\n        try self.init(\n            id: LegacyNotificationIdWrapper(type: .message, id: messageView.privateMessage.id).hashValue,\n            contentId: messageView.privateMessage.id,\n            read: messageView.privateMessage.read,\n            content: .message(.init(from: messageView))\n        )\n    }\n}\n\n// This can be removed once we drop support for < Lemmy 1.0\nprivate struct LegacyNotificationIdWrapper: Hashable {\n    let type: InboxNotificationContentType\n    let id: Int\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Instance1Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-14.\n//\n\nimport Foundation\n\npublic extension Instance1Snapshot {\n    init(from site: PieFedSite) throws(ApiClientError) {\n        self.init(\n            actorId: site.actorId,\n            // This is kinda dodgy\n            id: site.actorId.hashValue,\n            instanceId: site.actorId.hashValue,\n            created: Date(timeIntervalSince1970: 0),\n            updated: nil,\n            publicKey: \"\",\n            displayName: site.name,\n            description: site.sidebarMd ?? site.sidebar,\n            shortDescription: site.description,\n            avatar: site.icon,\n            banner: nil,\n            lastRefresh: Date(timeIntervalSince1970: 0),\n            contentWarning: nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Instance2Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-14.\n//\n\nimport Foundation\n\npublic extension Instance2Snapshot {\n    init(pieFed: PieFedSite, lemmy: PieFedLemmyCompatibleSiteView) throws(ApiClientError) {\n        // I suspect these can only be `nil` when the `PieFedSite` is used in a request body\n        \n        guard let enableDownvotes = pieFed.enableDownvotes else {\n            throw ApiClientError.responseMissingRequiredData(\"PieFedSite downvotesEnabled\")\n        }\n        \n        guard let userCount = pieFed.userCount else {\n            throw ApiClientError.responseMissingRequiredData(\"PieFedSite userCount\")\n        }\n        \n        guard let registrationMode = pieFed.registrationMode else {\n            throw ApiClientError.responseMissingRequiredData(\"PieFedSite registrationMode\")\n        }\n\n        let counts = lemmy.counts\n        let activeUserCount: ActiveUserCount = .init(\n            sixMonths: counts.usersActiveHalfYear,\n            month: counts.usersActiveMonth,\n            week: counts.usersActiveWeek,\n            day: counts.usersActiveDay\n        )\n\n        try self.init(\n            instance: .init(from: pieFed),\n            setup: true,\n            voteFederationMode: enableDownvotes ? .all : .downvotesDisabled,\n            nsfwContentEnabled: false,\n            communityCreationRestrictedToAdmins: false,\n            emailVerificationRequired: true,\n            applicationQuestion: nil,\n            isPrivate: false,\n            defaultTheme: \"browser\",\n            defaultFeed: .all,\n            legalInformation: nil,\n            hideModlogNames: true,\n            emailApplicationsToAdmins: true,\n            emailReportsToAdmins: false,\n            slurFilterRegex: nil,\n            actorNameMaxLength: 20,\n            federationEnabled: true,\n            captchaEnabled: false,\n            captchaDifficulty: nil,\n            registrationMode: .init(from: registrationMode),\n            federationSignedFetch: nil,\n            defaultPostListingMode: .list,\n            defaultPostSortType: .hot,\n            userCount: userCount,\n            postCount: counts.posts,\n            commentCount: counts.comments,\n            communityCount: counts.communities,\n            activeUserCount: activeUserCount\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Instance3Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-14.\n//\n\nimport Foundation\n\npublic extension Instance3Snapshot {\n    init(pieFed: PieFedGetSiteResponse, lemmy: PieFedLemmyCompatibleSiteResponse) throws(ApiClientError) {\n        // In addition to having their own site request, PieFed also impersonates\n        // Lemmy's site request at \"api/v3/site\". We also use that response here,\n        // because the response contains some data that is missing from PieFed's\n        // own site request.\n        \n        // The source code for this is here (function name: `lemmy_site_data`)\n        // https://codeberg.org/rimu/pyfedi/src/commit/75c48f6d22ec831e05bc54852f514caf34a60d0a/app/activitypub/util.py\n        \n        guard let allLanguages = pieFed.site.allLanguages else {\n            throw ApiClientError.responseMissingRequiredData(\"PieFedSite allLanguages\")\n        }\n        \n        var administrators: [Person2Snapshot] = []\n        administrators.reserveCapacity(pieFed.admins.count)\n        for admin in pieFed.admins {\n            try administrators.append(.init(from: admin))\n        }\n\n        try self.init(\n            instance: .init(pieFed: pieFed.site, lemmy: lemmy.siteView),\n            allLanguages: allLanguages.compactMap { .init($0) },\n            software: .init(type: .pieFed, version: .init(pieFed.version)),\n            allowedLanguageIds: .init(0 ... allLanguages.count - 1),\n            blockedUrls: [],\n            administrators: administrators\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/LegacySortTimeRangeLimit+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension LegacySortTimeRangeLimit {\n    var pieFedSortType: PieFedSortType {\n        switch self {\n        case .hour: .topHour\n        case .sixHour: .topSixHour\n        case .twelveHour: .topTwelveHour\n        case .day: .topDay\n        case .week: .topWeek\n        case .month: .topMonth\n        case .threeMonth: .topThreeMonths\n        case .sixMonth: .topSixMonths\n        case .nineMonth: .topNineMonths\n        case .year: .topYear\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/ListingType+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension ListingType {\n    init?(from listingType: PieFedListingType) {\n        let value: Self? = switch listingType {\n        case .all: .all\n        case .local: .local\n        case .subscribed: .subscribed\n        case .moderatorView, .moderating: .moderated\n        case .popular: .popular\n        }\n        if let value {\n            self = value\n        } else {\n            return nil\n        }\n    }\n    \n    var pieFedListingType: PieFedListingType? {\n        switch self {\n        case .all: .all\n        case .local: .local\n        case .subscribed: .subscribed\n        case .moderated: .moderatorView\n        case .popular: .popular\n        case .suggested: nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Locale.Language+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-14.\n//\n\nimport Foundation\n\nextension Locale.Language {\n    init?(_ language: PieFedLanguageView) {\n        if let code = language.code {\n            self = .init(identifier: code)\n        } else {\n            return nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Message1Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-25.\n//\n\nimport Foundation\n\npublic extension Message1Snapshot {\n    init(from message: PieFedPrivateMessage) throws(ApiClientError) {\n        var deleted = message.deleted\n        // This is required on PieFed 1.1. It *should* no longer be necessary\n        // from PieFed 1.2 onwards. Check to be sure though\n        if message.content == \"Message Deleted\" {\n            deleted = true\n        }\n\n        self.init(\n            actorId: message.apId,\n            id: message.id,\n            creatorId: message.creatorId,\n            recipientId: message.recipientId,\n            created: message.published,\n            content: message.content,\n            updated: message.updated,\n            read: message.read,\n            deleted: deleted\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Message2Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-25.\n//\n\nimport Foundation\n\npublic extension Message2Snapshot {\n    init(from message: PieFedPrivateMessageView) throws(ApiClientError) {\n        try self.init(\n            message: .init(from: message.privateMessage),\n            creator: .init(from: message.creator),\n            recipient: .init(from: message.recipient)\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Person1Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Person1Snapshot {\n    init(from person: PieFedPerson, allPropertiesPresent: Bool = false) throws(ApiClientError) {\n        self.init(\n            actorId: person.actorId,\n            id: person.id,\n            name: person.userName,\n            created: person.published,\n            instanceId: person.instanceId,\n            displayName: person.title ?? person.userName,\n            avatar: person.avatar,\n            banner: person.banner,\n            note: person.note,\n            updated: nil,\n            description: person.about,\n            matrixUserId: nil,\n            isBot: person.bot,\n            // Does PieFed not have bans with expiry times, or did they just not put it in the API yet?\n            instanceBan: person.banned ? .permanentlyBanned : .notBanned,\n            deleted: person.deleted,\n            allPropertiesPresent: allPropertiesPresent\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Person2Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Person2Snapshot {\n    init(from person: PieFedPersonView, allPropertiesPresent: Bool = false) throws(ApiClientError) {\n        try self.init(\n            person: .init(from: person.person, allPropertiesPresent: allPropertiesPresent),\n            isAdmin: person.isAdmin,\n            postCount: person.counts.postCount,\n            commentCount: person.counts.commentCount\n        )\n    }\n\n    init(from localUser: PieFedLocalUserView) throws(ApiClientError) {\n        try self.init(\n            person: .init(from: localUser.person, allPropertiesPresent: true),\n            isAdmin: false,\n            postCount: localUser.counts.postCount,\n            commentCount: localUser.counts.commentCount\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Person3Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Person3Snapshot {\n    init(from userInfo: PieFedMyUserInfo) throws(ApiClientError) {\n        var moderatedCommunities: [Community1Snapshot] = []\n        moderatedCommunities.reserveCapacity(userInfo.moderates.count)\n        \n        for moderate in userInfo.moderates {\n            try moderatedCommunities.append(.init(from: moderate.community))\n        }\n\n        try self.init(\n            person: .init(from: userInfo.localUserView),\n            site: nil,\n            moderatedCommunities: moderatedCommunities\n        )\n    }\n    \n    init(from personDetails: PieFedGetUserResponse) throws(ApiClientError) {\n        var moderatedCommunities: [Community1Snapshot] = []\n        moderatedCommunities.reserveCapacity(personDetails.moderates.count)\n        \n        for moderate in personDetails.moderates {\n            try moderatedCommunities.append(.init(from: moderate.community))\n        }\n        \n        try self.init(\n            person: .init(from: personDetails.personView, allPropertiesPresent: true),\n            site: personDetails.site.map { site throws(ApiClientError) in try .init(from: site) },\n            moderatedCommunities: moderatedCommunities\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Person4Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-25.\n//\n\nimport Foundation\n\npublic extension Person4Snapshot {\n    init(from userInfo: PieFedMyUserInfo) throws(ApiClientError) {\n        let user = userInfo.localUserView.localUser\n        try self.init(\n            person: .init(from: userInfo),\n            email: nil,\n            showNsfw: user.showNsfw,\n            theme: \"\",\n            defaultListingType: .init(from: user.defaultListingType) ?? .all,\n            interfaceLanguage: \"en\",\n            showAvatars: true,\n            sendNotificationsToEmail: false,\n            showScores: user.showScores,\n            showBotAccounts: user.showBotAccounts,\n            showReadPosts: user.showReadPosts,\n            discussionLanguageIds: Set(userInfo.discussionLanguages.compactMap(\\.id)),\n            emailVerified: true,\n            acceptedApplication: true,\n            openLinksInNewTab: nil,\n            blurNsfw: nil,\n            autoExpandImages: nil,\n            infiniteScrollEnabled: nil,\n            postListingMode: nil,\n            totp2faEnabled: false,\n            enableKeyboardNavigation: true,\n            enableAnimatedImages: nil,\n            collapseBotComments: false\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/PersonVoteSnapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-09-18.\n//\n\nimport Foundation\n\nextension PersonVoteSnapshot {\n    init(from vote: PieFedPostLikeView) throws(ApiClientError) {\n        try self.init(\n            creator: .init(from: vote.creator),\n            score: vote.score,\n            creatorBannedFromCommunity: vote.creatorBannedFromCommunity\n        )\n    }\n    \n    init(from vote: PieFedCommentLikeView) throws(ApiClientError) {\n        try self.init(\n            creator: .init(from: vote.creator),\n            score: vote.score,\n            creatorBannedFromCommunity: vote.creatorBannedFromCommunity\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/PersonalUnreadCountSnapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-04.\n//\n\nimport Foundation\n\npublic extension PersonalUnreadCountSnapshot {\n    init(from response: PieFedUserUnreadCountsResponse) throws(ApiClientError) {\n        self.replies = response.replies\n        self.mentions = response.mentions\n        self.messages = response.privateMessages\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Post1Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Post1Snapshot {\n    init(from post: PieFedPost) throws(ApiClientError) {\n        self.init(\n            actorId: post.apId,\n            id: post.id,\n            creatorId: post.userId,\n            communityId: post.communityId,\n            created: post.published,\n            title: post.title,\n            content: post.body,\n            linkUrl: post.url,\n            embed: nil,\n            poll: post.poll.map { .init(from: $0) },\n            nsfw: post.nsfw,\n            thumbnailUrl: post.thumbnailUrl,\n            updated: post.updated,\n            languageId: post.languageId,\n            altText: post.altText,\n            // If a post is removed, deleted is true for some reason\n            deleted: post.removed ? false : post.deleted,\n            removed: post.removed,\n            pinnedCommunity: post.sticky,\n            pinnedInstance: false,\n            locked: post.locked\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Post2Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Post2Snapshot {\n    init(from post: PieFedPostView, overrideRead: Bool? = nil) throws(ApiClientError) {\n        let votes = VotesModel(\n            upvotes: post.counts.upvotes,\n            downvotes: post.counts.downvotes,\n            myVote: .guaranteedInit(from: post.myVote)\n        )\n\n        try self.init(\n            post: .init(from: post.post),\n            creator: .init(from: post.creator),\n            community: .init(from: post.community),\n            commentCount: post.counts.comments,\n            unreadCommentCount: post.unreadComments,\n            creatorIsModerator: post.creatorIsModerator,\n            creatorIsAdmin: post.creatorIsAdmin,\n            creatorBannedFromCommunity: post.creatorBannedFromCommunity,\n            creatorBlocked: false,\n            votes: votes,\n            saved: post.saved,\n            read: overrideRead ?? post.read,\n            hidden: post.hidden\n        )\n    }\n    \n    init(from report: PieFedPostReportView) throws(ApiClientError) {\n        let votes = VotesModel(from: report.counts, myVote: .guaranteedInit(from: report.myVote))\n\n        try self.init(\n            post: .init(from: report.post),\n            creator: .init(from: report.postCreator),\n            community: .init(from: report.community),\n            commentCount: report.counts.comments,\n            unreadCommentCount: 0,\n            creatorIsModerator: report.creatorIsModerator,\n            creatorIsAdmin: report.creatorIsAdmin,\n            creatorBannedFromCommunity: report.creatorBannedFromCommunity,\n            creatorBlocked: report.creatorBlocked,\n            votes: votes,\n            saved: report.saved,\n            read: false,\n            hidden: false\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/Post3Snapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension Post3Snapshot {\n    init(from post: PieFedGetPostResponse) throws(ApiClientError) {\n        var crossPosts: [Post2Snapshot] = []\n        for crossPost in post.crossPosts {\n            try crossPosts.append(.init(from: crossPost))\n        }\n\n        try self.init(\n            post: .init(from: post.postView),\n            community: .init(from: post.communityView, allPropertiesPresent: false),\n            crossPosts: crossPosts\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/PostFeatureType+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-20.\n//\n\nimport Foundation\n\nextension PostFeatureType {\n    var piefedPostFeatureType: PieFedPostFeatureType {\n        switch self {\n        case .community: .community\n        case .instance: .local\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/PostPoll+PieFed.swift",
    "content": "//\n//  PostPoll+PieFed.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-01-26.\n//\n\nextension PostPoll {\n    init(from poll: PieFedPostPoll) {\n        self.endDate = poll.endPoll\n        self.latestVote = poll.latestVote\n        self.localOnly = poll.localOnly\n        self.type = .init(from: poll.mode)\n        let myVotes = poll.myVotes ?? []\n        self.choices = poll.choices.map { .init(from: $0, selected: myVotes.contains($0.id)) }\n    }\n}\n\nextension PostPollType {\n    init(from type: PieFedPostPollMode) {\n        self = switch type {\n        case .single: .single\n        case .multiple: .multiple\n        }\n    }\n}\n\nextension PostPollChoice {\n    init(from choice: PieFedPollChoice, selected: Bool) {\n        self.id = choice.id\n        self.label = choice.choiceText\n        self.voteCount = choice.numVotes\n        self.selected = selected\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/PostSortType+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension PostSortType {\n    var pieFedSortType: PieFedSortType? {\n        switch self {\n        case .active: nil\n        case .hot: .hot\n        case .new: .new\n        case .old: .old\n        case .mostComments: nil\n        case .newComments: .active // This is intentional\n        case .controversial: nil\n        case .scaled: .scaled\n        case let .top(range):\n            range.pieFedSortType\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/RegistrationMode+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-14.\n//\n\nimport Foundation\n\npublic extension RegistrationMode {\n    init(from mode: PieFedRegistrationMode) {\n        self = switch mode {\n        case .closed: .closed\n        case .open: .open\n        case .requireApplication: .requiresApplication\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/ReportSnapshot+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-25.\n//\n\nimport Foundation\n\nextension ReportSnapshot {\n    init(from report: PieFedCommentReportView) throws(ApiClientError) {\n        try self.init(\n            creator: .init(from: report.creator),\n            id: report.commentReport.id,\n            created: report.commentReport.published,\n            resolver: report.resolver.map { resolver throws(ApiClientError) in try .init(from: resolver) },\n            updated: report.commentReport.updated,\n            resolved: report.commentReport.resolved,\n            reason: report.commentReport.reason ?? \"\",\n            target: .comment(.init(from: report))\n        )\n    }\n    \n    init(from report: PieFedPostReportView) throws(ApiClientError) {\n        try self.init(\n            creator: .init(from: report.creator),\n            id: report.postReport.id,\n            created: report.postReport.published,\n            resolver: report.resolver.map { resolver throws(ApiClientError) in try .init(from: resolver) },\n            updated: report.postReport.updated,\n            resolved: report.postReport.resolved,\n            reason: report.postReport.reason,\n            target: .post(.init(from: report))\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/ResolvedContent+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-25.\n//\n\nimport Foundation\n\npublic extension ResolvedContent {\n    init(from response: PieFedResolveObjectResponse) throws {\n        if let comment = response.comment {\n            self = try .comment(.init(from: comment))\n        } else if let post = response.post {\n            self = try .post(.init(from: post))\n        } else if let community = response.community {\n            self = try .community(.init(from: community))\n        } else if let person = response.person {\n            self = try .person(.init(from: person))\n        } else {\n            throw ApiClientError.noEntityFound\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/SearchSortType+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension SearchSortType {\n    var pieFedSortType: PieFedSortType? {\n        switch self {\n        case .new: .new\n        case .old: nil\n        case let .top(range):\n            range.pieFedSortType\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PieFedExtensions/SortTimeRange+PieFed.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension SortTimeRange {\n    var pieFedSortType: PieFedSortType? {\n        switch self {\n        case .allTime: .topAll\n        case let .limited(timeInterval):\n            LegacySortTimeRangeLimit(timeInterval)?.pieFedSortType\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Post/Post+Conformance.swift",
    "content": "//\n//  Post+Conformance.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-07.\n//\n\nimport Foundation\nimport Nuke\nimport Rest\n\n// MARK: CacheIdentifiable\n\npublic extension Post {\n    var cacheId: Int { id }\n}\n\n// MARK: FeedLoadable\n\npublic extension Post {\n    typealias FilterType = PostFilterType\n    \n    static func == (lhs: Post, rhs: Post) -> Bool {\n        lhs.actorId == rhs.actorId\n    }\n    \n    func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        switch sortType {\n        case .new:\n            return .new(created)\n        }\n    }\n}\n\n// MARK: ImagePrefetchProviding\n\nextension Post: ImagePrefetchProviding {\n    public var type: PostType {\n        if let poll {\n            return .poll(poll)\n        }\n        // post with URL: image, embedded, or link\n        if let linkUrl {\n            if let embeddedMediaUrl {\n                return .embedded(embeddedMediaUrl, originalLink: linkUrl)\n            }\n            \n            // if image, return image link, otherwise return thumbnail\n            if linkUrl.isMedia {\n                return .media(linkUrl)\n            }\n            return .link(.init(content: linkUrl, thumbnail: thumbnailUrl, label: embed?.title ?? title))\n        }\n\n        // otherwise text, but post.body needs to be present, even if it's an empty string\n        if let postBody = content {\n            return .text(postBody)\n        }\n\n        return .titleOnly\n    }\n    \n    func parseLoopEmbeds() async {\n        if let loopsUrl = await linkUrl?.parseEmbeddedLoops() {\n            _ = await Task { @MainActor in\n                embeddedMediaUrl = loopsUrl\n            }.result\n        }\n    }\n    \n    public func imageRequests(configuration config: PrefetchingConfiguration) async -> [ImageRequest] {\n        var ret: [ImageRequest] = .init()\n        \n        // handle loops.video embedding\n        if config.embedLoops {\n            await parseLoopEmbeds()\n        }\n        \n        switch type {\n        case let .media(url), let .embedded(url, _):\n            // media/embedded media: only load the media\n            var urlRequest: URLRequest\n            switch config.imageSize {\n            case .unlimited:\n                urlRequest = mlemUrlRequest(url: url)\n            case let .limited(size):\n                urlRequest = mlemUrlRequest(url: url.withIconSize(size))\n            }\n            ret.append(ImageRequest(urlRequest: urlRequest, priority: .high))\n        case let .link(link):\n            // websites: load image and favicon\n            if config.fetchFavicons, let url = link.favicon {\n                let urlRequest = mlemUrlRequest(url: url)\n                ret.append(ImageRequest(urlRequest: urlRequest))\n            }\n            if let url = link.thumbnail {\n                var urlRequest: URLRequest\n                switch config.imageSize {\n                case .unlimited:\n                    urlRequest = mlemUrlRequest(url: url)\n                case let .limited(size):\n                    urlRequest = mlemUrlRequest(url: url.withIconSize(size))\n                }\n                ret.append(ImageRequest(urlRequest: urlRequest, priority: .high))\n            }\n        default:\n            break\n        }\n        // preload user and community avatars--fetching both because we don't know which we'll need, but these are super tiny\n        // so it's probably not an API crime, right?\n        if let avatarSize = config.avatarSize {\n            if let communityAvatarLink = community.value_?.avatar {\n                ret.append(ImageRequest(urlRequest: mlemUrlRequest(url: communityAvatarLink.withIconSize(avatarSize))))\n            }\n            \n            if let userAvatarLink = creator.value_?.avatar {\n                ret.append(ImageRequest(urlRequest: mlemUrlRequest(url: userAvatarLink.withIconSize(avatarSize))))\n            }\n        }\n        \n        return ret\n    }\n}\n\n// MARK: SelectableContentProviding\n\npublic extension Post {\n    var selectableContent: String? {\n        if let content {\n            \"\\(title)\\n\\n\\(content)\"\n        } else {\n            title\n        }\n    }\n}\n\n// MARK: ContentIdentifiable\n\npublic extension Post {\n    static var modelTypeId: ContentType { .post }\n}\n\n// MARK: Resolvable\n\npublic extension Post {\n    /// Returns a `URL` that can be resolved by another `ApiClient`.\n    func resolvableUrl(from instance: ContentModelUrlType) -> URL {\n        switch instance {\n        case .host: actorId.url\n        case .provider: .post(host: api.host, id: id)\n        }\n    }\n    \n    @inlinable\n    var allResolvableUrls: [URL] {\n        ContentModelUrlType.allCases.map { resolvableUrl(from: $0) }\n    }\n}\n\n// MARK: Sharable\n\npublic extension Post {\n    func url() -> URL { api.baseUrl.appending(path: \"post/\\(id)\") }\n}\n\n// MARK: InteractableProviding\n\npublic extension Post {\n    var downvotesEnabled: Bool {\n        api.voteFederationMode.postDownvote != .disable\n    }\n}\n\n// MARK: PersonContentProviding\n\npublic extension Post {\n    var userContent: PersonContent { .init(wrappedValue: .post(self)) }\n}\n\n// MARK: CanModerateProviding\n\npublic extension Post {\n    var canModerate: Bool {\n        guard let myPersonModerates = api.myPerson?.moderates else { return false }\n        return myPersonModerates(.id(communityId)) || api.isAdmin\n    }\n}\n\n// MARK: ReportableProviding\n\npublic extension Post {\n    func report(reason: String) async throws {\n        try await api.reportPost(id: id, reason: reason)\n    }\n}\n\n// MARK: OwnershipProviding\n\npublic extension Post {\n    func isOwnContent(myPersonId: Int) -> Bool {\n        creatorId == myPersonId\n    }\n}\n\n// MARK: Snapshots\n\npublic extension Post {\n    func takeSnapshot1() -> Post1Snapshot {\n        .init(actorId: actorId,\n              id: id,\n              creatorId: creatorId,\n              communityId: communityId,\n              created: created,\n              title: title,\n              content: content,\n              linkUrl: linkUrl,\n              embed: embed,\n              poll: poll,\n              nsfw: nsfw,\n              thumbnailUrl: thumbnailUrl,\n              updated: updated,\n              languageId: languageId,\n              altText: altText,\n              deleted: deleted,\n              removed: removed,\n              pinnedCommunity: pinnedCommunity,\n              pinnedInstance: pinnedInstance,\n              locked: locked\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Post/Post+Mock.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-02.\n//\n\nimport Foundation\n\n// TODO: updated mocks\n//#if DEBUG\n//    public extension Post1 {\n//        static func mock(\n//            api: MockApiClient = .mock,\n//            actorId: ActorIdentifier? = nil,\n//            id: Int,\n//            creatorId: Int,\n//            communityId: Int,\n//            created: Date,\n//            title: String,\n//            content: String?,\n//            linkUrl: URL?,\n//            deleted: Bool,\n//            embed: PostEmbed?,\n//            pinnedCommunity: Bool,\n//            pinnedInstance: Bool,\n//            locked: Bool,\n//            nsfw: Bool,\n//            removed: Bool,\n//            thumbnailUrl: URL?,\n//            updated: Date?,\n//            languageId: Int,\n//            altText: String?\n//        ) -> Post1 {\n//            .init(\n//                api: api,\n//                actorId: actorId ?? .init(url: URL(string: \"https://\\(api.host)/post/\\(id)\")!)!,\n//                id: id,\n//                creatorId: creatorId,\n//                communityId: communityId,\n//                created: created,\n//                title: title,\n//                content: content,\n//                linkUrl: linkUrl,\n//                deleted: deleted,\n//                embed: embed,\n//                pinnedCommunity: pinnedCommunity,\n//                pinnedInstance: pinnedInstance,\n//                locked: locked,\n//                nsfw: nsfw,\n//                removed: removed,\n//                thumbnailUrl: thumbnailUrl,\n//                updated: updated,\n//                languageId: languageId,\n//                altText: altText\n//            )\n//        }\n//    }\n//#endif\n\n//#if DEBUG\n//    public extension Post2 {\n//        static func mock(\n//            api: ApiClient = .mock,\n//            post1: Post1,\n//            creator: Person1,\n//            community: Community1,\n//            votes: VotesModel,\n//            creatorIsModerator: Bool,\n//            creatorIsAdmin: Bool,\n//            creatorBannedFromCommunity: Bool,\n//            commentCount: Int,\n//            unreadCommentCount: Int,\n//            saved: Bool,\n//            read: Bool,\n//            hidden: Bool\n//        ) -> Post2 {\n//            assert(api === post1.api)\n//            assert(api === creator.api)\n//            assert(api === community.api)\n//            return .init(\n//                api: api,\n//                post1: post1,\n//                creator: creator,\n//                community: community,\n//                votes: votes,\n//                creatorIsModerator: creatorIsModerator,\n//                creatorIsAdmin: creatorIsAdmin,\n//                creatorBannedFromCommunity: creatorBannedFromCommunity,\n//                commentCount: commentCount,\n//                unreadCommentCount: unreadCommentCount,\n//                saved: saved,\n//                read: read,\n//                hidden: hidden\n//            )\n//        }\n//    }\n//#endif\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Post/Post.swift",
    "content": "//\n//  Post.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-12-18.\n//\n\nimport Observation\nimport Foundation\nimport Nuke\nimport Rest\n\npublic struct PostEmbed: Equatable {\n    public let title: String?\n    public let description: String?\n    public let videoUrl: URL?\n}\n\n@Observable\npublic class Post:\n    UnifiedModelProviding,\n    FeedLoadable,\n    SelectableContentProviding,\n    ContentIdentifiable,\n    Resolvable,\n    Sharable,\n    UnifiedReadableProviding,\n    InteractableProviding,\n    PersonContentProviding,\n    DeletableProviding,\n    ReportableProviding,\n    RemovableProviding,\n    PurgableProviding {    \n    public typealias Properties = PostProperties\n    \n    public var api: ApiClient\n    private let properties: PostProperties\n    @ObservationIgnored lazy var updateQueue: UnifiedUpdateQueue<Post> = .init(parent: self, properties: properties)\n    \n    // MARK: Custom Properties\n    // Mlem-specific properties that are not reflected in the API\n    \n    public var readQueued: Bool = false\n    public var pinnedCommunityPending: Bool = false\n    public var pinnedInstancePending: Bool = false\n    public var lockedPending: Bool = false\n    public var nsfwPending: Bool = false\n    public var removedPending: Bool = false\n    public var purged: Bool = false\n    public var embeddedMediaUrl: URL?\n    \n    // MARK: API Properties\n    // Properties that are provided by the API\n\n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let creatorId: Int\n    public let communityId: Int\n    public let created: Date\n    public var title: String\n    public var content: String?\n    public var linkUrl: URL?\n    public var embed: PostEmbed?\n    public var poll: PostPoll?\n    public var nsfw: Bool\n    public var thumbnailUrl: URL?\n    public var updated: Date?\n    public var languageId: Int\n    public var altText: String?\n    public var deleted: Bool\n    public var removed: Bool\n    public var pinnedCommunity: Bool\n    public var pinnedInstance: Bool\n    public var locked: Bool\n    \n    public var creator: ExpectedValue<Person>\n    public var community: ExpectedValue<Community>\n    public var commentCount: ExpectedValue<Int>\n    public var unreadCommentCount: ExpectedValue<Int>\n    public var creatorIsModerator: ExpectedValue<Bool>\n    public var creatorIsAdmin: ExpectedValue<Bool>\n    public var creatorBannedFromCommunity: ExpectedValue<Bool>\n    public var creatorBlocked: ExpectedValue<Bool>\n    public var votes: ExpectedValue<VotesModel>\n    public var saved: ExpectedValue<Bool>\n    public var readStatus: ExpectedValue<Bool>\n    public var read: ExpectedValue<Bool> {\n        .init(\n            value: readStatus.value?.or(readQueued),\n            provideValue: { try await self.upgrade() })\n    }\n    public var hidden: ExpectedValue<Bool>\n    public var crossPosts: ExpectedValue<[Post]>\n    \n    // MARK: Initializers and Updates\n    \n    public init(api: ApiClient, properties: PostProperties) {\n        self.api = api\n        self.properties = properties\n        \n        self.actorId = properties.actorId\n        self.id = properties.id\n        self.creatorId = properties.creatorId\n        self.communityId = properties.communityId\n        self.created = properties.created\n        self.title = properties.title\n        self.content = properties.content\n        self.linkUrl = properties.linkUrl\n        self.embed = properties.embed\n        self.poll = properties.poll\n        self.nsfw = properties.nsfw\n        self.thumbnailUrl = properties.thumbnailUrl\n        self.updated = properties.updated\n        self.languageId = properties.languageId\n        self.altText = properties.altText\n        self.deleted = properties.deleted\n        self.removed = properties.removed\n        self.pinnedCommunity = properties.pinnedCommunity\n        self.pinnedInstance = properties.pinnedInstance\n        self.locked = properties.locked\n        \n        // because upgrade() is not available until all properties are initialized, first populate all properties\n        // with ExpectedValues that don't actually do anything, then reassign them properly at the end of the init\n        // this is somewhat cumbersome but avoids lazy vars, which are very awkward in Observables\n        self.creator = dummyExpectedValue(properties.creator)\n        self.community = dummyExpectedValue(properties.community)\n        self.commentCount = dummyExpectedValue(properties.commentCount)\n        self.unreadCommentCount = dummyExpectedValue(properties.unreadCommentCount)\n        self.creatorIsModerator = dummyExpectedValue(properties.creatorIsModerator)\n        self.creatorIsAdmin = dummyExpectedValue(properties.creatorIsAdmin)\n        self.creatorBannedFromCommunity = dummyExpectedValue(properties.creatorBannedFromCommunity)\n        self.creatorBlocked = dummyExpectedValue(properties.creatorBlocked)\n        self.votes = dummyExpectedValue(properties.votes)\n        self.saved = dummyExpectedValue(properties.saved)\n        self.readStatus = dummyExpectedValue(properties.read)\n        self.hidden = dummyExpectedValue(properties.hidden)\n        self.crossPosts = dummyExpectedValue(properties.crossPosts)\n        \n        func expectedValue<T>(_ value: T?) -> ExpectedValue<T> {\n            .init(\n                value: value,\n                provideValue: { try await self.upgrade() })\n        }\n        self.creator = expectedValue(properties.creator)\n        self.community = expectedValue(properties.community)\n        self.commentCount = expectedValue(properties.commentCount)\n        self.unreadCommentCount = expectedValue(properties.unreadCommentCount)\n        self.creatorIsModerator = expectedValue(properties.creatorIsModerator)\n        self.creatorIsAdmin = expectedValue(properties.creatorIsAdmin)\n        self.creatorBannedFromCommunity = expectedValue(properties.creatorBannedFromCommunity)\n        self.creatorBlocked = expectedValue(properties.creatorBlocked)\n        self.votes = expectedValue(properties.votes)\n        self.saved = expectedValue(properties.saved)\n        self.readStatus = expectedValue(properties.read)\n        self.hidden = expectedValue(properties.hidden)\n        self.crossPosts = expectedValue(properties.crossPosts)\n    }\n    \n    @MainActor\n    public func update(with properties: PostProperties) {\n        setIfChanged(\\.title, properties.title)\n        setIfChanged(\\.content, properties.content)\n        setIfChanged(\\.linkUrl, properties.linkUrl)\n        setIfChanged(\\.embed, properties.embed)\n\n        // PieFed has a bug where sometimes doesn't it return the poll object.\n        // This check prevents the poll from disappearing if we get a response\n        // that doesn't include it.\n        if properties.poll != nil {\n            setIfChanged(\\.poll, properties.poll)\n        }\n\n        setIfChanged(\\.nsfw, properties.nsfw)\n        setIfChanged(\\.thumbnailUrl, properties.thumbnailUrl)\n        setIfChanged(\\.updated, properties.updated)\n        setIfChanged(\\.languageId, properties.languageId)\n        setIfChanged(\\.altText, properties.altText)\n        setIfChanged(\\.deleted, properties.deleted)\n        setIfChanged(\\.removed, properties.removed)\n        setIfChanged(\\.pinnedCommunity, properties.pinnedCommunity)\n        setIfChanged(\\.pinnedInstance, properties.pinnedInstance)\n        setIfChanged(\\.locked, properties.locked)\n\n        // creator and community are not expected to change value, but need to be assigned if absent\n        setIfNil(\\.creator.value_, properties.creator ?? creator.value_)\n        setIfNil(\\.community.value_, properties.community ?? community.value_)\n        updateIfChanged(\\.commentCount.value_, properties.commentCount ?? commentCount.value_)\n        updateIfChanged(\\.unreadCommentCount.value_, properties.unreadCommentCount ?? unreadCommentCount.value_)\n        updateIfChanged(\\.creatorIsModerator.value_, properties.creatorIsModerator ?? creatorIsModerator.value_)\n        updateIfChanged(\\.creatorIsAdmin.value_, properties.creatorIsAdmin ?? creatorIsAdmin.value_)\n        updateIfChanged(\\.creatorBannedFromCommunity.value_ , properties.creatorBannedFromCommunity ?? creatorBannedFromCommunity.value_)\n        updateIfChanged(\\.creatorBlocked.value_, properties.creatorBlocked ?? creatorBlocked.value_)\n        updateIfChanged(\\.votes.value_, properties.votes ?? votes.value_)\n        updateIfChanged(\\.saved.value_, properties.saved ?? saved.value_)\n        updateIfChanged(\\.readStatus.value_, properties.read ?? readStatus.value_)\n        updateIfChanged(\\.hidden.value_, properties.hidden ?? hidden.value_)\n        updateIfChanged(\\.crossPosts.value_, properties.crossPosts ?? crossPosts.value_)\n    }\n    \n    @MainActor\n    public func softUpdate(with properties: PostProperties) {\n        setIfNil(\\.creator.value_, properties.creator)\n        setIfNil(\\.community.value_, properties.community)\n        setIfNil(\\.commentCount.value_, properties.commentCount)\n        setIfNil(\\.unreadCommentCount.value_, properties.unreadCommentCount)\n        setIfNil(\\.creatorIsModerator.value_, properties.creatorIsModerator)\n        setIfNil(\\.creatorIsAdmin.value_, properties.creatorIsAdmin)\n        setIfNil(\\.creatorBannedFromCommunity.value_ , properties.creatorBannedFromCommunity)\n        setIfNil(\\.creatorBlocked.value_, properties.creatorBlocked)\n        setIfNil(\\.votes.value_, properties.votes)\n        setIfNil(\\.saved.value_, properties.saved)\n        setIfNil(\\.readStatus.value_, properties.read)\n        setIfNil(\\.hidden.value_, properties.hidden)\n        setIfNil(\\.crossPosts.value_, properties.crossPosts ?? crossPosts.value_)\n    }\n    \n    // MARK: Upgrades\n    \n    public func upgrade() async throws {\n        try await updateQueue.upgrade()\n    }\n    \n    public func refresh() async throws {\n        try await updateQueue.refresh()\n    }\n    \n    public func fetchUpgraded() async throws -> PostProperties {\n        let snapshot = try await api.repository.getPost(id: id)\n        return await .init(api: api, snapshot: .post3(snapshot))\n    }\n    \n    public func resolve(with api: ApiClient) async throws -> Self {\n        let stub = PostStub(api: api, url: allResolvableUrls[0])\n        return try await stub.getPost() as! Self\n    }\n}\n\n// MARK: - Computed\n\npublic extension Post {\n    var linkHost: String? {\n        if case let .link(link) = type {\n            return link.host\n        }\n        return nil\n    }\n    \n    var isOwnPost: Bool { creatorId == api.myPerson?.id }\n}\n\n// MARK: - Interactions\n\npublic extension Post {\n    \n    // Vote\n    \n    var updateVote: ((ScoringOperation) -> Void)? {\n        if let votes = votes.value {\n            return { self.updateVote($0, votes: votes) }\n        }\n        return nil\n    }\n    \n    private func updateVote(_ newValue: ScoringOperation, votes: VotesModel) {\n        self.votes.value_ = votes.applyScoringOperation(operation: newValue)\n        readStatus.value_ = true\n        \n        Task {\n            await updateQueue.addItem {\n                try await .init(\n                    api: self.api,\n                    snapshot: .post2(self.api.repository.voteOnPost(id: self.id, score: newValue)))\n            }\n        }\n    }\n    \n    // Save\n    \n    func updateSaved(_ newValue: Bool) {\n        saved.value_ = newValue\n        readStatus.value_ = true\n        \n        Task {\n            await updateQueue.addItem {\n                try await .init(\n                    api: self.api,\n                    snapshot: .post2(self.api.repository.savePost(id: self.id, save: newValue)))\n            }\n        }\n    }\n    \n    // Reply\n    \n    func reply(content: String, languageId: Int?) async throws -> Comment {\n        try await self.api.replyToPost(id: id, content: content, languageId: languageId)\n    }\n    \n    // Hide\n    \n    func updateHidden(_ newValue: Bool) {\n        hidden.value_ = newValue\n        readStatus.value_ = true\n        \n        Task {\n            await updateQueue.addItem { properties in\n                try await self.api.repository.hidePost(id: self.id, hide: newValue)\n                var properties = properties\n                properties.hidden = newValue\n                return properties\n            }\n        }\n    }\n    \n    // Read\n    \n    func updateRead(_ newValue: Bool, shouldQueue: Bool = false) {\n        if shouldQueue {\n            readQueued = newValue\n            Task {\n                if newValue {\n                    await api.markReadQueue.add(id)\n                } else {\n                    await api.markReadQueue.remove(id)\n                }\n            }\n        } else {\n            readStatus.value_ = newValue\n            Task {\n                await updateQueue.addItem { properties in\n                    try await self.api.repository.markPostAsRead(id: self.id, read: newValue)\n                    var properties = properties\n                    properties.read = newValue\n                    return properties\n                }\n            }\n        }\n    }\n    \n    /// Update the post when its queued mark read operation completes.\n    func queuedMarkReadCompleted() {\n        // sending this through the updateQueue ensures queue.lastVerifiedSnapshot receives the correct read value\n        Task {\n            await updateQueue.addItem { properties in\n                var properties = properties\n                properties.read = true\n                return properties\n            }\n            readQueued = false\n        }\n    }\n\n    // Vote on Poll\n\n    func voteInPoll(_ choiceIds: Set<Int>) {\n        guard let poll = self.poll else { return }\n        self.poll = poll.applyVoteChoices(choiceIds: choiceIds)\n        \n        Task {\n            await updateQueue.addItem {\n                try await .init(\n                    api: self.api,\n                    snapshot: .post2(self.api.repository.voteInPoll(postId: self.id, choiceIds: choiceIds)))\n            }\n        }\n    }\n    \n    // Pin\n    \n    /// Pins or unpins this post to the community according to newValue\n    /// - Parameters:\n    ///   - newValue: true to pin post, false to unpin\n    ///   - callback: if present, when the repository call completes, is called with `.success` if the operation succeeded and `.failure` otherwise.\n    func updatePinnedCommunity(_ newValue: Bool, callback: ((UpdateStatus) -> Void)?) {\n        pinnedCommunity = newValue\n        pinnedCommunityPending = true\n        \n        Task {\n            await updateQueue.addItem {\n                do {\n                    let ret = try await self.api.repository.pinPost(id: self.id, pin: newValue, to: .community)\n                    callback?(.success)\n                    return await .init(api: self.api, snapshot: .post2(ret))\n                } catch {\n                    callback?(.failure(error))\n                    throw error\n                }\n            }\n        }\n    }\n    \n    /// Pins or unpins this post to the instance according to newValue\n    /// - Parameters:\n    ///   - newValue: true to pin post, false to unpin\n    ///   - callback: if present, when the repository call completes, is called with `.success` if the operation succeeded and `.failure` otherwise.\n    func updatePinnedInstance(_ newValue: Bool, callback: ((UpdateStatus) -> Void)?) {\n        pinnedInstance = newValue\n        pinnedInstancePending = true\n        \n        Task {\n            await updateQueue.addItem {\n                do {\n                    let ret = try await self.api.repository.pinPost(id: self.id, pin: newValue, to: .instance)\n                    callback?(.success)\n                    return await .init(api: self.api, snapshot: .post2(ret))\n                } catch {\n                    callback?(.failure(error))\n                    throw error\n                }\n            }\n        }\n    }\n       \n    \n    // Lock\n    \n    /// Locks or unlocks this post according to newValue\n    /// - Parameters:\n    ///   - newValue: true to lock post, false to unlock\n    ///   - callback: if present, when the repository call completes, is called with `.success` if the operation succeeded and `.failure` otherwise.\n    func updateLocked(_ newValue: Bool, callback: ((UpdateStatus) -> Void)?) {\n        locked = newValue\n        lockedPending = true\n        Task {\n            await updateQueue.addItem {\n                do {\n                    let ret = try await self.api.repository.lockPost(id: self.id, lock: newValue)\n                    callback?(.success)\n                    return await .init(api: self.api, snapshot: .post2(ret))\n                } catch {\n                    callback?(.failure(error))\n                    throw (error)\n                }\n            }\n        }\n    }\n    \n    // Get Comments\n    \n    func getComments(\n        sort: CommentSortType = .hot,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment] {\n        try await api.getComments(\n            postId: id,\n            sort: sort,\n            page: page,\n            maxDepth: maxDepth,\n            limit: limit,\n            filter: filter\n        )\n    }\n    \n    // Edit\n    \n    func edit(\n        title: String,\n        content: String?,\n        linkUrl: URL?,\n        altText: String?,\n        thumbnail: URL?,\n        nsfw: Bool,\n        languageId: Int?\n    ) throws {\n        self.title = title\n        self.content = content\n        self.linkUrl = linkUrl\n        self.altText = altText\n        self.thumbnailUrl = thumbnail\n        self.nsfw = nsfw\n        self.languageId = languageId ?? self.languageId\n        \n        Task {\n            await updateQueue.addItem {\n                await .init(api: self.api, snapshot: .post2(try await self.api.repository.editPost(\n                    id: self.id,\n                    title: title,\n                    content: content,\n                    linkUrl: linkUrl,\n                    altText: altText,\n                    thumbnail: thumbnail,\n                    nsfw: nsfw,\n                    languageId: languageId\n                )))\n            }\n        }\n    }\n    \n    // Get Votes\n    \n    func getVotes(page: Int, limit: Int) async throws -> [PersonVote] {\n        try await api.getPostVotes(id: id, communityId: communityId, page: page, limit: limit)\n    }\n    \n    // Deleted\n    \n    func updateDeleted(_ newValue: Bool, callback: ((UpdateStatus) -> Void)?) {\n        deleted = newValue\n        \n        Task {\n            await updateQueue.addItem {\n                do {\n                    let snapshot = try await self.api.repository.deletePost(id: self.id, delete: newValue)\n                    callback?(.success)\n                    return await .init(api: self.api, snapshot: .post2(snapshot))\n                } catch {\n                    callback?(.failure(error))\n                    throw error\n                }\n            }\n        }\n    }\n    \n    // NSFW\n    \n    func updateNsfw(_ newValue: Bool, callback: ((UpdateStatus) -> Void)?) {\n        nsfw = newValue\n        nsfwPending = true\n        \n        Task {\n            await updateQueue.addItem {\n                do {\n                    let snapshot = try await self.api.repository.setPostNsfw(id: self.id, nsfw: newValue)\n                    callback?(.success)\n                    return await .init(api: self.api, snapshot: .post1(snapshot))\n                } catch {\n                    callback?(.failure(error))\n                    throw (error)\n                }\n            }\n        }\n    }\n    \n    // Remove\n    \n    func updateRemoved(_ newValue: Bool, reason: String?, callback: ((UpdateStatus) -> Void)?) {\n        removed = newValue\n        removedPending = true\n        \n        Task {\n            await updateQueue.addItem {\n                do {\n                    let snapshot = try await self.api.repository.removePost(id: self.id, remove: newValue, reason: reason)\n                    callback?(.success)\n                    return await .init(api: self.api, snapshot: .post2(snapshot))\n                } catch {\n                    callback?(.failure(error))\n                    throw (error)\n                }\n            }\n        }\n    }\n    \n    // Purge\n    \n    func purge(reason: String?) async throws {\n        try await api.purgePost(id: id, reason: reason)\n        purged = true\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Post/PostPoll.swift",
    "content": "//\n//  PostPoll.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-01-26.\n//\n\nimport Foundation\n\npublic struct PostPoll: Hashable {\n    public let endDate: Date?\n    public let localOnly: Bool?\n    public let latestVote: Date?\n    public let type: PostPollType\n\n    public var choices: [PostPollChoice]\n\n    public var hasEnded: Bool {\n        if let endDate {\n            endDate < .now\n        } else {\n            false\n        }\n    }\n\n    // For multi-choice polls, this will be greater than the\n    // number of users who have voted in the poll\n    public var totalVotes: Int {\n        choices.compactMap(\\.voteCount).reduce(0, +)\n    }\n\n    public var hasVoted: Bool {\n        choices.contains { $0.selected }\n    }\n\n    func applyVoteChoices(choiceIds: Set<Int>) -> PostPoll {\n        var new = self\n        new.choices = []\n        for choice in self.choices {\n            var choice = choice\n            let newSelected = choiceIds.contains(choice.id)\n            if choice.selected {\n                choice.voteCount = (choice.voteCount ?? 0) - 1\n            }\n            if newSelected {\n                choice.voteCount = (choice.voteCount ?? 0) + 1\n            }\n            choice.selected = newSelected\n            new.choices.append(choice)\n        }\n        return new\n    }\n}\n\npublic enum PostPollType {\n    case single, multiple\n}\n\npublic struct PostPollChoice: Hashable {\n    public let id: Int\n    public let label: String\n    public var voteCount: Int?\n    public var selected: Bool\n\n    public func percentage(poll: PostPoll) -> Int {\n        if poll.totalVotes == 0 {\n            0\n        } else {\n            Int(100 * Double(voteCount ?? 0) / Double(poll.totalVotes))\n        }\n    }\n}\n\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Post/PostProperties.swift",
    "content": "//\n//  PostProperties.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-07.\n//\n\nimport Foundation\n\npublic struct PostProperties: UnifiedPropertiesProviding {\n    // From Post1Snapshot, guaranteed to always be present\n    let actorId: ActorIdentifier\n    let id: Int\n    let creatorId: Int\n    let communityId: Int\n    let created: Date\n    var title: String\n    var content: String?\n    var linkUrl: URL?\n    var embed: PostEmbed?\n    var poll: PostPoll?\n    var nsfw: Bool\n    var thumbnailUrl: URL?\n    var updated: Date?\n    var languageId: Int\n    var altText: String?\n    var deleted: Bool\n    var removed: Bool\n    var pinnedCommunity: Bool\n    var pinnedInstance: Bool\n    var locked: Bool\n    \n    // From Post2Snapshot\n    var creator: Person?\n    var community: Community?\n    var commentCount: Int?\n    var unreadCommentCount: Int?\n    var creatorIsModerator: Bool?\n    var creatorIsAdmin: Bool?\n    var creatorBannedFromCommunity: Bool?\n    var creatorBlocked: Bool?\n    var votes: VotesModel?\n    var saved: Bool?\n    var read: Bool?\n    var hidden: Bool?\n    \n    // From Post3Snapshot\n    var crossPosts: [Post]?\n    \n    /// Constructs a PostProperties from a given snapshot\n    @MainActor\n    public init(api: ApiClient, snapshot: AnyPostSnapshot) {\n        let snapshot1: Post1Snapshot\n        let snapshot2: Post2Snapshot?\n        let snapshot3: Post3Snapshot?\n        switch snapshot {\n        case let .post1(post1Snapshot):\n            snapshot1 = post1Snapshot\n            snapshot2 = nil\n            snapshot3 = nil\n        case let .post2(post2Snapshot):\n            snapshot1 = post2Snapshot.post\n            snapshot2 = post2Snapshot\n            snapshot3 = nil\n        case let .post3(post3Snapshot):\n            snapshot1 = post3Snapshot.post.post\n            snapshot2 = post3Snapshot.post\n            snapshot3 = post3Snapshot\n        }\n        \n        if let snapshot3 {\n            crossPosts = api.caches.post.getModels(api: api, from: snapshot3.crossPosts.map { .post2($0) })\n        }\n        \n        if let snapshot2 {\n            let newCreator = api.caches.person.getModel(api: api, from: .person1(snapshot2.creator))\n            newCreator.updateKnownCommunityBanState(id: snapshot1.communityId, banned: snapshot2.creatorBannedFromCommunity)\n            \n            creator = newCreator\n            community =  api.caches.community.getModel(api: api, from: .community1(snapshot2.community))\n            commentCount = snapshot2.commentCount\n            unreadCommentCount = snapshot2.unreadCommentCount\n            creatorIsModerator = snapshot2.creatorIsModerator\n            creatorIsAdmin = snapshot2.creatorIsAdmin\n            creatorBannedFromCommunity = snapshot2.creatorBannedFromCommunity\n            creatorBlocked = snapshot2.creatorBlocked\n            votes = snapshot2.votes\n            saved = snapshot2.saved\n            read = snapshot2.read\n            hidden = snapshot2.hidden\n        }\n        \n        actorId = snapshot1.actorId\n        id = snapshot1.id\n        creatorId = snapshot1.creatorId\n        communityId = snapshot1.communityId\n        created = snapshot1.created\n        title = snapshot1.title\n        content = snapshot1.content\n        linkUrl = snapshot1.linkUrl\n        embed = snapshot1.embed\n        poll = snapshot1.poll\n        nsfw = snapshot1.nsfw\n        thumbnailUrl = snapshot1.thumbnailUrl\n        updated = snapshot1.updated\n        languageId = snapshot1.languageId\n        altText = snapshot1.altText\n        deleted = snapshot1.deleted\n        removed = snapshot1.removed\n        pinnedCommunity = snapshot1.pinnedCommunity\n        pinnedInstance = snapshot1.pinnedInstance\n        locked = snapshot1.locked\n    }\n    \n    public mutating func merge(_ other: PostProperties) {\n        // tier 1 properties: simple assignment\n        self.title = other.title\n        self.content = other.content\n        self.linkUrl = other.linkUrl\n        self.embed = other.embed\n        self.poll = other.poll\n        self.nsfw = other.nsfw\n        self.thumbnailUrl = other.thumbnailUrl\n        self.updated = other.updated\n        self.languageId = other.languageId\n        self.altText = other.altText\n        self.deleted = other.deleted\n        self.removed = other.removed\n        self.pinnedCommunity = other.pinnedCommunity\n        self.pinnedInstance = other.pinnedInstance\n        self.locked = other.locked\n        \n        // tier 2, 3 properties: only assign if incoming non-nil\n        self.creator = other.creator ?? self.creator\n        self.community = other.community ?? self.community\n        self.commentCount = other.commentCount ?? self.commentCount\n        self.unreadCommentCount = other.unreadCommentCount ?? self.unreadCommentCount\n        self.creatorIsModerator = other.creatorIsModerator ?? self.creatorIsModerator\n        self.creatorIsAdmin = other.creatorIsAdmin ?? self.creatorIsAdmin\n        self.creatorBannedFromCommunity = other.creatorBannedFromCommunity ?? self.creatorBannedFromCommunity\n        self.creatorBlocked = other.creatorBlocked ?? self.creatorBlocked\n        self.votes = other.votes ?? self.votes\n        self.saved = other.saved ?? self.saved\n        self.read = other.read ?? self.read\n        self.hidden = other.hidden ?? self.hidden\n        \n        self.crossPosts = other.crossPosts ?? self.crossPosts\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Post/PostStub.swift",
    "content": "//\n//  PostStub.swift\n//  Mlem\n//\n//  Created by Sjmarf on 16/02/2024.\n//\n\nimport Foundation\n\npublic struct PostStub: Hashable {\n    public var api: ApiClient\n    public var url: URL\n    \n    public init(api: ApiClient, url: URL) {\n        self.api = api\n        self.url = url\n    }\n    \n    public func asLocal() -> Self {\n        .init(api: .getApiClient(url: url.removingPathComponents(), username: nil), url: url)\n    }\n    \n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(url)\n    }\n    \n    public static func == (lhs: PostStub, rhs: PostStub) -> Bool {\n        lhs.url == rhs.url\n    }\n    \n    public func getPost() async throws -> Post {\n        try await api.getPost(url: resolvableUrl)\n    }\n}\n\n// Resolvable conformance\npublic extension PostStub {\n    var resolvableUrl: URL { url }\n    \n    @inlinable\n    var allResolvableUrls: [URL] { [resolvableUrl] }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PostFeatureType.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic enum PostFeatureType {\n    case community, instance\n    \n    init(from featureType: LemmyPostFeatureType) {\n        self = switch featureType {\n        case .community: .community\n        case .local: .instance\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PostFeedViewMode.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic enum PostFeedViewMode {\n    case list, card, smallCard\n    \n    init(from listingMode: LemmyPostListingMode) {\n        self = switch listingMode {\n        case .list: .list\n        case .card: .card\n        case .smallCard: .smallCard\n        }\n    }\n    \n    var apiType: LemmyPostListingMode {\n        switch self {\n        case .list: .list\n        case .card: .card\n        case .smallCard: .smallCard\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PostLink.swift",
    "content": "//\n//  PostLink.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-30.\n//\n\nimport Foundation\n\npublic struct PostLink: Hashable {\n    public let content: URL\n    public let thumbnail: URL?\n    public let label: String\n    \n    public var favicon: URL? {\n        if let baseUrl = content.host {\n            return URL(string: \"https://www.google.com/s2/favicons?sz=64&domain=\\(baseUrl)\")\n        }\n        return nil\n    }\n\n    public var host: String {\n        if var host = content.host() {\n            host.trimPrefix(\"www.\")\n            return host\n        }\n        return \"website\"\n    }\n    \n    public var effectiveThumbnail: URL? {\n        thumbnail ?? content.youTubeThumbnailUrl\n    }\n    \n    public init(content: URL, thumbnail: URL?, label: String) {\n        self.content = content\n        self.thumbnail = thumbnail\n        self.label = label\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PostType.swift",
    "content": "//\n//  PostType.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-06-14.\n//\n\nimport Foundation\n\npublic enum PostType: Hashable {\n    /// Post containing both a title and text\n    case text(String)\n    /// Post containing only media\n    case media(URL)\n    /// Link post with embedded media content\n    case embedded(URL, originalLink: URL)\n    /// Link post\n    case link(PostLink)\n    /// Poll post\n    case poll(PostPoll)\n    /// Post containing only a title\n    case titleOnly\n    \n    public var isText: Bool {\n        if case .text = self {\n            return true\n        }\n        return false\n    }\n    \n    public var isMedia: Bool {\n        switch self {\n        case .media, .embedded: return true\n        default: return false\n        }\n    }\n    \n    public var isLink: Bool {\n        if case .link = self {\n            return true\n        }\n        return false\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Profile/ProfileProviding.swift",
    "content": "//\n//  ProfileProviding.swift\n//\n//\n//  Created by Sjmarf on 20/05/2024.\n//\n\nimport Foundation\n\npublic protocol ProfileProviding: ActorIdentifiable {\n    var name: String { get }\n    var avatar: URL? { get }\n    var blocked: any RealizedValueProviding<Bool> { get }\n    \n    var displayName: String { get }\n    var description: String? { get }\n    var banner: URL? { get }\n    var profileCreated: Date? { get }\n    var updated: Date? { get }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/PurgableProviding.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-10-27.\n//\n\nimport Foundation\n\npublic protocol PurgableProviding: ContentIdentifiable {\n    var purged: Bool { get }\n    func purge(reason: String?) async throws\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ReadableProviding.swift",
    "content": "//\n//  ReadableProviding.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-12-04.\n//\n\npublic protocol UnifiedReadableProviding {\n    var read: ExpectedValue<Bool> { get }\n}\n\n// TODO: Full unified models remove\npublic protocol ReadableProviding {\n    var read: Bool { get }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/RegistrationApplication/RegistrationApplication.swift",
    "content": "//\n//  RegistrationApplication1.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-01-12.\n//\n\nimport Foundation\n\n// This could be two-tiered but doing so is tricky because whether the application has\n// been approved or denied is unknown at tier 1, which would make the tier 1 model pretty\n// useless. At this time, only tier 2 applications are returned anyways.\n\n@Observable\npublic final class RegistrationApplication: ContentIdentifiable, FeedLoadable {\n    public typealias FilterType = ModMailItemFilterType\n    \n    public static let modelTypeId: ContentType = .registrationApplication\n    public let api: ApiClient\n    \n    public let id: Int\n    public internal(set) var questionResponse: String\n    public let creator: Person\n    public internal(set) var resolver: Person?\n    public internal(set) var email: String?\n    public internal(set) var emailVerified: Bool\n    public internal(set) var showNsfw: Bool\n    public let created: Date\n    \n    var resolutionManager: StateManager<ResolutionState>\n    public var resolution: ResolutionState { resolutionManager.displayedValue }\n    \n    init(\n        api: ApiClient,\n        id: Int,\n        questionResponse: String,\n        creator: Person,\n        resolver: Person?,\n        email: String?,\n        emailVerified: Bool,\n        showNsfw: Bool,\n        created: Date,\n        resolution: ResolutionState\n    ) {\n        self.api = api\n        self.id = id\n        self.questionResponse = questionResponse\n        self.creator = creator\n        self.resolver = resolver\n        self.email = email\n        self.emailVerified = emailVerified\n        self.showNsfw = showNsfw\n        self.created = created\n        self.resolutionManager = .init(wrappedValue: resolution)\n        resolutionManager.onSet = { newValue, type, _ in\n            if type == .begin || type == .rollback {\n                api.unreadCount?.updateUnverifiedItem(itemType: .registrationApplication, isRead: newValue != .unresolved)\n            }\n        }\n        resolutionManager.onVerify = { newValue, _ in\n            api.unreadCount?.verifyItem(itemType: .registrationApplication, isRead: newValue != .unresolved)\n        }\n    }\n    \n    var modMailId: Int {\n        var hasher: Hasher = .init()\n        hasher.combine(\"application\")\n        hasher.combine(id)\n        return hasher.finalize()\n    }\n    \n    public func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        switch sortType {\n        case .new: .new(created)\n        }\n    }\n    \n    @discardableResult\n    public func approve() -> Task<StateUpdateResult, Never> {\n        resolutionManager.performRequest(expectedResult: .approved) { semaphore in\n            try await self.api.approveRegistrationApplication(id: self.id, semaphore: semaphore)\n        }\n    }\n    \n    @discardableResult\n    public func deny(reason: String?) -> Task<StateUpdateResult, Never> {\n        resolutionManager.performRequest(expectedResult: .denied(reason: reason)) { semaphore in\n            try await self.api.denyRegistrationApplication(id: self.id, reason: reason, semaphore: semaphore)\n        }\n    }\n}\n\npublic extension RegistrationApplication {\n    enum ResolutionState: Equatable {\n        case unresolved, approved, denied(reason: String?)\n        \n        public var reason: String? {\n            switch self {\n            case .unresolved: nil\n            case .approved: nil\n            case let .denied(reason): reason\n            }\n        }\n        \n        public var isDenied: Bool {\n            switch self {\n            case .denied: true\n            default: false\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/RegistrationMode.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic enum RegistrationMode {\n    case closed, open, requiresApplication\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/RemovableProviding.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-15.\n//\n\nimport Foundation\n\n// TODO: it should be possible to move some of the queueing logic into RemovableProviding and just have the model\n// provide the repository call\n\npublic protocol RemovableProviding: ContentIdentifiable, CanModerateProviding {\n    var removed: Bool { get }\n    var removedPending: Bool { get }\n    \n    func updateRemoved(_ newValue: Bool, reason: String?, callback: ((UpdateStatus) -> Void)?)\n}\n\npublic extension RemovableProviding {\n    /// Toggles the removed status of this item\n    /// - Parameters: callback: if present, when the repository call completes, is called with `.success` if the operation succeeded and `.failure` otherwise.\n    func toggleRemoved(reason: String?, callback: ((UpdateStatus) -> Void)? = nil) {\n        updateRemoved(!removed, reason: reason, callback: callback)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Report/Report.swift",
    "content": "//\n//  Report.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-15.\n//\n\nimport Foundation\nimport Observation\n\n@Observable\npublic class Report: CacheIdentifiable, ContentModel, FeedLoadable {\n    public typealias FilterType = ModMailItemFilterType\n    \n    public var api: ApiClient\n    \n    // Keep this internal - a post report and a comment report can have the same ID, so it's not a true identifier.\n    var id: Int\n    \n    public let created: Date\n    public internal(set) var updated: Date?\n    public let creator: Person\n    public let target: ReportTarget\n    public internal(set) var resolver: Person?\n    public internal(set) var reason: String\n    \n    var resolvedManager: StateManager<Bool>\n    public var resolved: Bool { resolvedManager.displayedValue }\n    \n    init(\n        api: ApiClient,\n        id: Int,\n        creator: Person,\n        resolver: Person?,\n        target: ReportTarget,\n        resolved: Bool,\n        reason: String,\n        created: Date,\n        updated: Date?\n    ) {\n        self.api = api\n        self.id = id\n        self.creator = creator\n        self.resolver = resolver\n        self.target = target\n        self.reason = reason\n        self.created = created\n        self.updated = updated\n        \n        self.resolvedManager = .init(wrappedValue: resolved)\n        resolvedManager.onSet = { newValue, type, _ in\n            if type == .begin || type == .rollback {\n                api.unreadCount?.updateUnverifiedItem(itemType: target.type.inboxItemType, isRead: newValue)\n            }\n        }\n        resolvedManager.onVerify = { newValue, _ in\n            api.unreadCount?.verifyItem(itemType: target.type.inboxItemType, isRead: newValue)\n        }\n    }\n    \n    public var modMailId: Int {\n        var hasher: Hasher = .init()\n        hasher.combine(\"report\")\n        hasher.combine(target.case)\n        hasher.combine(id)\n        return hasher.finalize()\n    }\n    \n    @discardableResult\n    public func updateResolved(_ newValue: Bool) -> Task<StateUpdateResult, Never> {\n        resolvedManager.performRequest(expectedResult: newValue) { semaphore in\n            switch self.target.type {\n            case .post:\n                try await self.api.resolvePostReport(id: self.id, resolved: newValue, semaphore: semaphore)\n            case .comment:\n                try await self.api.resolveCommentReport(id: self.id, resolved: newValue, semaphore: semaphore)\n            case .message:\n                try await self.api.resolveMessageReport(id: self.id, resolved: newValue, semaphore: semaphore)\n            }\n        }\n    }\n    \n    @discardableResult\n    public func toggleResolved() -> Task<StateUpdateResult, Never> {\n        updateResolved(!resolved)\n    }\n    \n    public func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        switch sortType {\n        case .new: .new(created)\n        }\n    }\n    \n    public static func == (lhs: Report, rhs: Report) -> Bool {\n        lhs.target.case == rhs.target.case && lhs.id == rhs.id\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Report/ReportTarget.swift",
    "content": "//\n//  ReportTarget.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-16.\n//\n\nimport Foundation\n\npublic enum ReportTarget {\n    enum Case {\n        case post, comment, message\n    }\n    \n    var `case`: Case {\n        switch self {\n        case .post: .post\n        case .comment: .comment\n        case .message: .message\n        }\n    }\n    \n    case post(Post)\n    case comment(Comment)\n    case message(Message2)\n    \n    var type: ReportType {\n        switch self {\n        case .post: .post\n        case .comment: .comment\n        case .message: .message\n        }\n    }\n    \n    public var community: Community? {\n        switch self {\n        case let .post(post): post.community.value_\n        case let .comment(comment): comment.community.value_\n        case .message: nil\n        }\n    }\n    \n    // TODO: UnifiedCommentModel, UnifiedMessageModel remove this shim\n    public var creator: ExpectedValue<Person> {\n        switch self {\n        case let .post(post): post.creator\n        case let .comment(comment): comment.creator\n        case let .message(message): .init(\n            value: message.creator,\n            provideValue: { fatalError(\"This should not be called\") }\n        )\n        }\n    }\n    \n    @MainActor\n    init(from snapshot: ReportTargetSnapshot, api: ApiClient, myPersonId: Int) {\n        switch snapshot {\n        case let .post(post):\n            self = .post(api.caches.post.getModel(api: api, from: .post2(post)))\n        case let .comment(comment):\n            self = .comment(api.caches.comment.getModel(api: api, from: .comment2(comment)))\n        case let .message(message):\n            self = .message(api.caches.message2.getModel(api: api, from: message, myPersonId: myPersonId))\n        }\n    }\n    \n    @MainActor\n    func update(with snapshot: ReportTargetSnapshot) {\n        // TODO: UpdateQueue rework reports to integrate UpdateQueue\n        switch (self, snapshot) {\n        case (.post, .post), (.comment, .comment):\n            break\n        case let (.message(message), .message(updatedMessage)):\n            message.update(with: updatedMessage)\n        default:\n            assertionFailure()\n        }\n    }\n}\n\npublic enum ReportType: Hashable {\n    case post, comment, message\n    \n    var inboxItemType: InboxItemType {\n        switch self {\n        case .post: .postReport\n        case .comment: .commentReport\n        case .message: .messageReport\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ReportUnreadCountSnapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic struct ReportUnreadCountSnapshot {\n    let comments: Int\n    let posts: Int\n    let messages: Int\n    \n    init(from response: LemmyGetReportCountResponse) throws(ApiClientError) {\n        self.comments = response.commentReports\n        self.posts = response.postReports\n        self.messages = response.privateMessageReports ?? 0\n    }\n    \n    var unreadCountDictionary: [InboxItemType: Int] {\n        [\n            .postReport: posts,\n            .commentReport: comments,\n            .messageReport: messages\n        ]\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ReportableProviding.swift",
    "content": "//\n//  ReportableProviding.swift\n//\n//\n//  Created by Sjmarf on 23/07/2024.\n//\n\nimport Foundation\n\npublic protocol ReportableProviding: OwnershipProviding {\n    func report(reason: String) async throws\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Resolvable.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-01-26.\n//\n\nimport Foundation\n\n// TODO: UnifiedCommunity move resolve(with: ApiClient) into this protocol\npublic protocol Resolvable {\n    /// An array of available URLs for this entity that can be resolved by another `ApiClient`.\n    var allResolvableUrls: [URL] { get }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/ResolvedContent.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-07.\n//\n\nimport Foundation\n\npublic enum ResolvedContent {\n    case comment(Comment2Snapshot)\n    case post(Post2Snapshot)\n    case community(Community2Snapshot)\n    case person(Person2Snapshot)\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/SelectableContentProviding.swift",
    "content": "//\n//  SelectableContentProviding.swift\n//\n//\n//  Created by Sjmarf on 02/07/2024.\n//\n\nimport Foundation\n\npublic protocol SelectableContentProviding: ActorIdentifiable {\n    var selectableContent: String? { get }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Sharable.swift",
    "content": "//\n//  Sharable.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-03-09.\n//\n\nimport Foundation\n\npublic protocol Sharable: ActorIdentifiable, Hashable {\n    func url() -> URL\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Shared/UnifiedModelProviding.swift",
    "content": "//\n//  UnifiedModelProviding.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-07.\n//\n\npublic protocol UnifiedModelProviding: AnyObject, CacheIdentifiable, ContentModel {\n    associatedtype Properties: UnifiedPropertiesProviding\n    \n    /// Updates with the values from the given Properties, preferring the incoming values. Should only be called\n    /// from the `UpdateQueue`\n    @MainActor func update(with properties: Properties)\n    \n    /// Updates only values that are currently nil with values from the given Properties. Safe to call outside the `UpdateQueue`\n    @MainActor func softUpdate(with properties: Properties)\n    \n    /// Retrieves a fully populated Properties for this model\n    func fetchUpgraded() async throws -> Properties\n    \n    func resolve(with api: ApiClient) async throws -> Self\n}\n\nextension UnifiedModelProviding {\n    @MainActor\n    func setIfChanged<T: Equatable>(_ keyPath: ReferenceWritableKeyPath<Self, T>, _ value: T) {\n        if self[keyPath: keyPath] != value {\n            self[keyPath: keyPath] = value\n        }\n    }\n    \n    /// If the provided value is non-nil and different from the current value at the target key path, updates\n    /// the target key path with the provided value\n    @MainActor\n    func updateIfChanged<T: Equatable>(_ keyPath: ReferenceWritableKeyPath<Self, T?>, _ value: T?) {\n        if let value, self[keyPath: keyPath] != value {\n            self[keyPath: keyPath] = value\n        }\n    }\n    \n    /// If the current value at the target key path is nil, udpates it with the provided value\n    @MainActor\n    func setIfNil<T>(_ keyPath: ReferenceWritableKeyPath<Self, T?>, _ value: T?) {\n        if self[keyPath: keyPath] == nil {\n            self[keyPath: keyPath] = value\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Shared/UnifiedPropertiesProviding.swift",
    "content": "//\n//  UnifiedPropertiesProviding.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-07.\n//\n\npublic protocol UnifiedPropertiesProviding {\n    /// Merges the given properties into this one, preferring the incoming properties\n    mutating func merge(_ other: Self)\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Shared/ValueProviding/ExpectedValue.swift",
    "content": "//\n//  ExpectedValue.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-07.\n//\n\nimport Foundation\n\n/// Represents a value that a model can have, but might not currently be fetched if the model was instantiated\n/// with a low-tier snapshot.\npublic struct ExpectedValue<T>: ValueProviding {\n    public var value_: T?\n    \n    /// Provides the value, or nil if the value is not present\n    public var value: T? {\n        if let ret = value_ { return ret }\n        Task {\n            do {\n                try await provideValue()\n            } catch {\n                print(error)\n            }\n        }\n        return nil\n    }\n    \n    /// Callback expected to update value_\n    let provideValue: () async throws -> Void\n    \n    init(value: T?, provideValue: @escaping () async throws -> Void) {\n        self.value_ = value\n        self.provideValue = provideValue\n    }\n}\n\nfunc dummyExpectedValue<T>(_ value: T?) -> ExpectedValue<T> {\n    .init(\n        value: value,\n        provideValue: { assertionFailure(\"Dummy function! This should not be called\") })\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Shared/ValueProviding/RealizedValue.swift",
    "content": "//\n//  RealizedValue.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-02-25.\n//\n\nimport Observation\n\n@Observable\npublic class RealizedValue<T>: RealizedValueProviding {\n    private var value_: T\n    public var value: T? { value_ }\n    public var realizedValue: T { value_ }\n    \n    public init(_ value: T) {\n        self.value_ = value\n    }\n    \n    public func set(_ newValue: T) {\n        value_ = newValue\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Shared/ValueProviding/SyntheticExpectedValue.swift",
    "content": "//\n//  SyntheticExpectedValue.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-02-26.\n//\n\nimport Observation\n\n@Observable\npublic class SyntheticExpectedValue<T: MergeableValue>: ValueSynthesizer<T?>, ValueProviding {\n    /// Callback expected to update value_\n    let provideValue: () async throws -> Void\n    \n    public var value: T? {\n        if value_ == nil {\n            Task {\n                do {\n                    try await provideValue()\n                } catch {\n                    print(error)\n                }\n            }\n        }\n        return synthesize()\n    }\n    \n    init(value: T?, provideValue: @escaping () async throws -> Void, mergeType: ValueMergeType) {\n        self.provideValue = provideValue\n        super.init(value: value, mergeType: mergeType)\n    }\n}\n\nfunc dummySyntheticExpectedValue<T>(_ value: T?) -> SyntheticExpectedValue<T> {\n    .init(\n        value: value,\n        provideValue: { assertionFailure(\"Dummy function! This should not be called\") },\n        mergeType: .disjunctive)\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Shared/ValueProviding/SyntheticRealizedValue.swift",
    "content": "//\n//  SyntheticRealizedValue.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-02-26.\n//\n\nimport Observation\n\n@Observable\npublic class SyntheticRealizedValue<T: MergeableValue>: ValueSynthesizer<T>, RealizedValueProviding {\n    public var value: T? { synthesize() }\n    public var realizedValue: T { synthesize() }\n    \n    public func set(_ newValue: T) {\n        value_ = newValue\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Shared/ValueProviding/ValueProviding.swift",
    "content": "//\n//  Untitled.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-02-25.\n//\n\n/// Represents any member of the value providing family\npublic protocol ValueProviding<T> {\n    associatedtype T\n    \n    var value: T? { get }\n}\n\n/// Represents a value that is guaranteed to be realized, but may need to be treated interchangeably with\n/// other `ValueProviding`s\npublic protocol RealizedValueProviding<T>: ValueProviding {\n    var realizedValue: T { get }\n    \n    // NOTE: while value_ is currently always T (not T?), so could theoretically be directly exposed as `T { get set }`,\n    // it is intentionally obscured behind this setter for extensibility\n    func set(_ newValue: T)\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Shared/ValueProviding/ValueSynthesizer.swift",
    "content": "//\n//  ValueSynthesizer.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-02-25.\n//\n\nimport Foundation\n\npublic enum ValueMergeType {\n    /// Indicates value should be true when any merged value is true\n    case disjunctive\n    \n    /// Indicates value should be true only when all merged values are true\n    case conjunctive\n}\n\npublic protocol MergeableValue: Equatable {\n    /// Merges self with other using the given merge type.\n    /// - Returns: result of the merged value\n    func merge(with other: Self, using mergeType: ValueMergeType) -> Self\n}\n\n// Allows optionals to be used as MergeableValue\nextension Optional: MergeableValue where Wrapped: MergeableValue & Equatable {\n    /// If both self and other are present, returns the result of merging them; otherwise returns whichever value is present,\n    /// and nil if both are nil.\n    public func merge(with other: Optional<Wrapped>, using mergeType: ValueMergeType) -> Optional<Wrapped> {\n        if let other {\n            return self?.merge(with: other, using: mergeType) ?? other\n        }\n        return self\n    }\n}\n\n/// Provides methods for tracking sibling `ValueSynthesizer`s. When `synthesize()` is called, all sibling values\n/// are accumulated into a single result according to the specified `mergeType`\n@Observable\npublic class ValueSynthesizer<T: MergeableValue> {\n    internal let uid: NSUUID = .init()\n    internal let mergeType: ValueMergeType\n    \n    // using NSMapTable to store weak references\n    internal var siblings: NSMapTable<NSUUID, ValueSynthesizer> = .weakToWeakObjects()\n    \n    public var value_: T\n    \n    public init(value: T, mergeType: ValueMergeType) {\n        self.value_ = value\n        self.mergeType = mergeType\n    }\n    \n    internal func synthesize() -> T {\n        siblings.dictionaryRepresentation().values.reduce(value_) { result, sibling in\n            result.merge(with: sibling.value_, using: mergeType)\n        }\n    }\n    \n    public func addSibling(_ sibling: ValueSynthesizer) {\n        siblings.setObject(sibling, forKey: sibling.uid)\n    }\n    \n    public func removeSibling(_ sibling: ValueSynthesizer) {\n        siblings.removeObject(forKey: sibling.uid)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/SignUpResponse.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic enum SignUpResponse {\n    public enum Reason: Hashable {\n        case awaitingApproval, awaitingEmailVerification\n    }\n    \n    case canLogIn(token: String)\n    case cannotLogIn(reasons: Set<Reason>)\n    \n    init(from loginResponse: LemmyLoginResponse) {\n        if let token = loginResponse.jwt {\n            self = .canLogIn(token: token)\n        }\n        var reasons: Set<Reason> = []\n        if loginResponse.registrationCreated {\n            reasons.insert(.awaitingApproval)\n        }\n        if loginResponse.verifyEmailSent {\n            reasons.insert(.awaitingEmailVerification)\n        }\n        if !reasons.isEmpty { assertionFailure() }\n        self = .cannotLogIn(reasons: reasons)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Sort Types/CommentSortType.swift",
    "content": "//\n//  CommentSortType.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-03-04.\n//\n\nimport SwiftUI\n\npublic enum CommentSortType: Hashable, Sendable {\n    case new\n    case old\n    case hot\n    case controversial\n    \n    /// From 1.0.0 onwards, any time interval is supported.\n    /// Before 1.0.0, only `.allTime` is supported.\n    case top(SortTimeRange)\n    \n    public var isTop: Bool {\n        switch self {\n        case .top: true\n        default: false\n        }\n    }\n    \n    public static var nonTopCases: [Self] = [\n        .hot,\n        .new,\n        .old,\n        .controversial\n    ]\n    \n    public static var legacyCases: [Self] = nonTopCases + [.top(.allTime)]\n    \n    public var timeRange: SortTimeRange? {\n        switch self {\n        case let .top(timeRange): timeRange\n        default: nil\n        }\n    }\n    \n    // This should only be used internally within ApiClient\n    var timeRangeSeconds: Int? { timeRange?.timeRangeSeconds }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Sort Types/LegacySortTimeRange.swift",
    "content": "//\n//  LegacySortTimeRange.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-25.\n//\n\nimport Foundation\n\n/// Represents the available \"top\" sort time ranges available before Lemmy 1.0.0.\n/// After 1.0.0, the top sort time range can be any number of seconds.\npublic enum LegacySortTimeRangeLimit: CaseIterable {\n    case hour\n    case sixHour\n    case twelveHour\n    case day\n    case week\n    case month\n    case threeMonth\n    case sixMonth\n    case nineMonth\n    case year\n}\n\npublic extension LegacySortTimeRangeLimit {\n    init?(_ timeInterval: TimeInterval?) {\n        if let match = Self.allCases.first(where: { $0.timeInterval == timeInterval }) {\n            self = match\n        } else {\n            return nil\n        }\n    }\n    \n    var timeInterval: TimeInterval {\n        let hour = 3600.0\n        let day = hour * 24\n        let month = day * 30\n        \n        return switch self {\n        case .hour: hour\n        case .sixHour: hour * 6\n        case .twelveHour: hour * 12\n        case .day: day\n        case .week: day * 7\n        case .month: month\n        case .threeMonth: month * 3\n        case .sixMonth: month * 6\n        case .nineMonth: month * 9\n        case .year: day * 365\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Sort Types/PostSortType.swift",
    "content": "//\n//  PostSortTimeRange.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-25.\n//\n\nimport Foundation\n\npublic enum PostSortType: Hashable, Sendable {\n    case active\n    case hot\n    case new\n    case old\n    case mostComments\n    case newComments\n    case controversial\n    case scaled\n    \n    /// From 1.0.0 onwards, any time interval is supported.\n    /// Before 1.0.0, there is a discrete list of supported time intervals,\n    /// represented by the ``LegacySortTimeRange`` type.\n    case top(SortTimeRange)\n    \n    public var isTop: Bool {\n        switch self {\n        case .top: true\n        default: false\n        }\n    }\n    \n    public static var nonTopCases: [Self] = [\n        .hot,\n        .scaled,\n        .active,\n        .new,\n        .old,\n        .controversial,\n        .newComments,\n        .mostComments\n    ]\n    \n    public static var legacyTopCases: [Self] = SortTimeRange.legacyCases.map { .top($0) }\n    \n    public static var legacyCases: [Self] = nonTopCases + legacyTopCases\n    \n    public var timeRange: SortTimeRange? {\n        switch self {\n        case let .top(timeRange): timeRange\n        default: nil\n        }\n    }\n    \n    // This should only be used internally within ApiClient\n    var timeRangeSeconds: Int? { timeRange?.timeRangeSeconds }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Sort Types/SearchSortType.swift",
    "content": "//\n//  SearchSortTimeRange.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-28.\n//\n\nimport Foundation\n\n// In the v3 API, it was possible to search for posts using any\n// of the regular post sorts (\"Hot\", \"Active\" etc). This was\n// intentionally removed in v4.\n// https://github.com/LemmyNet/lemmy/issues/5401\n\npublic enum SearchSortType: Hashable, Sendable {\n    case new\n    case old\n    \n    /// From 1.0.0 onwards, any time interval is supported.\n    /// Before 1.0.0, there is a discrete list of supported time intervals,\n    /// represented by the ``LegacySortTimeRange`` type.\n    case top(SortTimeRange)\n    \n    public var isTop: Bool {\n        switch self {\n        case .top: true\n        default: false\n        }\n    }\n    \n    public static var nonTopCases: [Self] = [.new, .old]\n    public static var legacyTopCases: [Self] = SortTimeRange.legacyCases.map { .top($0) }\n    public static var legacyCases: [Self] = nonTopCases + legacyTopCases\n    public static var legacyPersonCases: [Self] = nonTopCases + [.top(.allTime)]\n    \n    public var timeRange: SortTimeRange? {\n        switch self {\n        case let .top(timeRange): timeRange\n        default: nil\n        }\n    }\n\n    // This should only be used internally within ApiClient\n    var timeRangeSeconds: Int? { timeRange?.timeRangeSeconds }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/Sort Types/SortTimeRange.swift",
    "content": "//\n//  SortTimeRange.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-03-01.\n//\n\nimport Foundation\n\npublic enum SortTimeRange: Hashable, Sendable {\n    case allTime\n    case limited(TimeInterval)\n    \n    public static func limited(_ timeRangeLimit: LegacySortTimeRangeLimit) -> Self {\n        .limited(timeRangeLimit.timeInterval)\n    }\n    \n    init?(_ apiSortType: LemmySortType) {\n        if apiSortType == .topAll {\n            self = .allTime\n        } else if let legacyTimeRange = LegacySortTimeRangeLimit(apiSortType) {\n            self = .limited(legacyTimeRange)\n        } else {\n            return nil\n        }\n    }\n    \n    public static var legacyCases: [Self] = LegacySortTimeRangeLimit.allCases.map { .limited($0) } + [.allTime]\n \n    // This should only be used internally within ApiClient\n    var timeRangeSeconds: Int {\n        switch self {\n        case .allTime: Int(Int32.max) // Going higher than this value causes an error\n        case let .limited(value): Int(value)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/StateManager.swift",
    "content": "//\n//  StateManager.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-03.\n//\n\nimport Foundation\nimport os\n\n// TODO: Unified models remove\n\n// These can't go inside of StateManager because generic classes cannot store static properties\nclass SemaphoreServer {\n    static var value: UInt = 0\n    \n    static func next() -> UInt {\n        value += 1\n        return value\n    }\n}\n\nenum StateManagerUpdateType: Equatable {\n    case begin\n    case rollback\n    case receive\n}\n\nprotocol StateManagerTickerProtocol {\n    var valid: Bool { get }\n    func begin(semaphore: UInt)\n    func rollback(semaphore: UInt)\n}\n\nstruct StateManagerTicket<Value: Equatable>: StateManagerTickerProtocol {\n    let manager: StateManager<Value>\n    let expectedResult: Value\n    \n    func begin(semaphore: UInt) {\n        manager.beginOperation(expectedResult: expectedResult, semaphore: semaphore)\n    }\n    \n    func rollback(semaphore: UInt) {\n        manager.rollback(semaphore: semaphore)\n    }\n    \n    var valid: Bool {\n        manager.wrappedValue != expectedResult\n    }\n}\n\n/// This class provides logic to ensure proper handling of returned API responses so as to avoid flickering or the client falling out of sync.\n/// When you begin a task, call `.beginVotingOperation` with the result you expect to get. If no other operations are ongoing,\n/// `lastVerifiedValue` is updated to match; otherwise `lastVerifiedValue` is left untouched. The semaphore is incremented\n/// and its value returned. When a vote finishes successfully, call `finishVotingOperation` with the new state returned from the server.\n/// If the caller is the most recent one, then clean state is wiped and a `true` value is returned; this indicates that the caller is clear to\n/// update the post with the returned value. If the caller is not the most recent one (i.e., another vote is underway), then the clean state is\n/// updated but a `false` value is returned; this indicates that the caller should not update the post with the returned value. When a vote\n/// finishes unsuccessfully, call `rollback`. If the caller is the most recent one, then the  `wrappedValue` will be reset to the\n/// `lastVerifiedValue`.\n@Observable\npublic class StateManager<Value: Equatable> {\n    let log: Logger = .mlemLogger()\n    \n    /// Underlying state-faked wrapped value\n    private(set) var wrappedValue: Value\n    \n    /// The state-faked value that should be shown to the user\n    public var displayedValue: Value { wrappedValue }\n    \n    /// Called when `wrappedValue` is changed.\n    var onSet: (Value, _ type: StateManagerUpdateType, _ semaphore: UInt?) -> Void\n    \n    /// Called whenever `wrappedValue` is verified.\n    var onVerify: (Value, _ semaphore: UInt?) -> Void\n    \n    /// Responsible for tracking who the most recent caller is. Every time the state is changed, `lastSemaphore` is incremented by one.\n    private var lastSemaphore: UInt = 0\n    \n    /// Responsible for tracking the last verified value. If the current value is in sync with the server, this will be nil.\n    private var lastVerifiedValue: Value?\n    \n    public var isInSync: Bool { lastVerifiedValue == nil }\n    public var verifiedValue: Value { lastVerifiedValue ?? wrappedValue }\n    \n    init(\n        wrappedValue: Value,\n        onSet: @escaping (Value, _ type: StateManagerUpdateType, _ semaphore: UInt?) -> Void = { _, _, _ in },\n        onVerify: @escaping (Value, _ semaphore: UInt?) -> Void = { _, _ in }\n    ) {\n        self.wrappedValue = wrappedValue\n        self.onSet = onSet\n        self.onVerify = onVerify\n    }\n        \n    /// Call at the start of a voting operation, BEFORE state faking is performed. Updates the clean state if nil and increments semaphore.\n    /// - Returns: new sempaphore value\n    @discardableResult\n    func beginOperation(expectedResult: Value, semaphore: UInt? = nil) -> UInt {\n        let semaphore = semaphore ?? SemaphoreServer.next()\n        lastSemaphore = semaphore\n        log.debug(\"[\\(semaphore)] began operation.\")\n        if lastVerifiedValue == nil {\n            log.debug(\"[\\(semaphore)] Set lastVerifiedValue to \\(String(describing: self.wrappedValue)).\")\n            lastVerifiedValue = wrappedValue\n        }\n        if wrappedValue != expectedResult {\n            wrappedValue = expectedResult\n            log.debug(\"[\\(semaphore)] Set wrappedValue to \\(String(describing: expectedResult)).\")\n            onSet(expectedResult, .begin, semaphore)\n        }\n        return lastSemaphore\n    }\n    \n    /// Call when we receive a value from the ApiClient that we *know* to be up-to-date. Optionally pass a `sempahore` value. If the StateManager is awaiting the result of an operation, the `wrappedValue` will *only* be set if the passed semaphore matches the one that the `StateManager` is waiting for. Otherwise, the value is saved as the `lastVerifiedValue` such that the `StateManager` will rollback to it if the in-progress operation fails.\n    @discardableResult\n    func updateWithReceivedValue(_ newState: Value, semaphore: UInt?) -> Bool {\n        if lastVerifiedValue == nil {\n            if wrappedValue != newState {\n                Task { @MainActor in\n                    self.wrappedValue = newState\n                    self.onSet(newState, .receive, semaphore)\n                }\n            }\n            return false\n        }\n        \n        if lastSemaphore == semaphore {\n            log.debug(\"[\\(semaphore?.description ?? \"nil\")] is the last caller! Resetting lastVerifiedValue.\")\n            onVerify(newState, semaphore)\n            lastVerifiedValue = nil\n            return true\n        }\n        \n        if lastVerifiedValue != newState {\n            lastVerifiedValue = newState\n            if semaphore != nil {\n                log.debug(\"[\\(semaphore?.description ?? \"nil\")] is not the last caller! Updating lastVerifiedValue to \\(String(describing: self.wrappedValue)).\")\n            }\n        }\n        return false\n    }\n    \n    /// If the given semaphore is still the most recent operation, rollback `wrappedValue` to `cleanValue`.\n    @discardableResult\n    func rollback(semaphore: UInt) -> Value? {\n        if lastSemaphore == semaphore, let lastVerifiedValue {\n            log.debug(\"[\\(semaphore)] is the most recent caller! Resetting lastVerifiedValue.\")\n            if wrappedValue != lastVerifiedValue {\n                wrappedValue = lastVerifiedValue\n                onSet(lastVerifiedValue, .rollback, semaphore)\n            }\n            defer { self.lastVerifiedValue = nil }\n            return lastVerifiedValue\n        } else {\n            log.debug(\"[\\(semaphore)] is not the most recent caller or vote state nil.\")\n            return nil\n        }\n    }\n    \n    func performRequest(\n        expectedResult: Value,\n        operation: @escaping (_ semaphore: UInt) async throws -> Void,\n        onRollback: @escaping (_ value: Value) -> Void = { _ in }\n    ) -> Task<StateUpdateResult, Never> {\n        Task(priority: .userInitiated) { @MainActor in\n            guard wrappedValue != expectedResult else { return .ignored }\n            \n            let semaphore = beginOperation(expectedResult: expectedResult)\n            do {\n                try await operation(semaphore)\n                return .succeeded\n            } catch {\n                log.error(\"Semaphore failed: \\(error.localizedDescription)\")\n                if let newValue = self.rollback(semaphore: semaphore) {\n                    onRollback(newValue)\n                }\n                return .failed\n            }\n        }\n    }\n    \n    func ticket(_ expectedResult: Value) -> StateManagerTicket<Value> {\n        StateManagerTicket(manager: self, expectedResult: expectedResult)\n    }\n}\n\nfunc groupStateRequest(\n    _ tickets: [any StateManagerTickerProtocol],\n    operation: @escaping (_ semaphore: UInt) async throws -> Void\n) -> Task<StateUpdateResult, Never> {\n    let semaphore = SemaphoreServer.next()\n    \n    let tickets = tickets.filter(\\.valid)\n    \n    for ticket in tickets {\n        ticket.begin(semaphore: semaphore)\n    }\n    return Task(priority: .userInitiated) {\n        do {\n            try await operation(semaphore)\n            return .succeeded\n        } catch {\n            Logger.universal.error(\"StateManager [\\(semaphore)] failed: \\(error.localizedDescription)\")\n            for ticket in tickets {\n                ticket.rollback(semaphore: semaphore)\n            }\n            return .failed\n        }\n    }\n}\n\nfunc groupStateRequest(\n    _ tickets: (any StateManagerTickerProtocol)...,\n    operation: @escaping (_ semaphore: UInt) async throws -> Void\n) -> Task<StateUpdateResult, Never> {\n    groupStateRequest(tickets, operation: operation)\n}\n\npublic enum StateUpdateResult {\n    case succeeded\n    case failed\n    /// Returned when the action is queued for later, e.g. when a post is marked as read.\n    case deferred\n    case ignored\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/SubscriptionList.swift",
    "content": "//\n//  SubscriptionList.swift\n//\n//\n//  Created by Sjmarf on 05/05/2024.\n//\n\nimport Observation\n\n@Observable\npublic class SubscriptionList {\n    /// All subscribed-to communities, including favorited communities.\n    public private(set) var communities: Set<Community> = .init() {\n        didSet {\n            communityIds = .init(communities.map(\\.id))\n        }\n    }\n\n    public private(set) var communityIds: Set<Int> = .init()\n    public private(set) var favorites: [Community] = .init()\n    public private(set) var alphabeticSections: [String?: [Community]] = .init()\n    public private(set) var instanceSections: [String?: [Community]] = .init()\n    \n    public internal(set) var hasLoaded: Bool = false\n    \n    var favoriteIDs: Set<Int> {\n        get { getFavorites() }\n        set { setFavorites(newValue) }\n    }\n    \n    private var getFavorites: () -> Set<Int>\n    private var setFavorites: (Set<Int>) -> Void\n    \n    private var api: ApiClient\n    \n    init(\n        apiClient: ApiClient,\n        getFavorites: @escaping () -> Set<Int>,\n        setFavorites: @escaping (Set<Int>) -> Void\n    ) {\n        self.api = apiClient\n        self.getFavorites = getFavorites\n        self.setFavorites = setFavorites\n    }\n    \n    public func refresh() async throws {\n        _ = try await api.getSubscriptionList()\n    }\n    \n    public func isFavorited(_ community: Community) -> Bool {\n        favoriteIDs.contains(community.id)\n    }\n    \n    private func alphabeticCategoryForCommunity(_ community: Community) -> String? {\n        let first = String(community.name.first ?? \"#\").folding(options: .diacriticInsensitive, locale: .current)\n        guard first.first?.isLetter ?? false else { return nil }\n        return first.uppercased()\n    }\n    \n    @MainActor\n    func updateCommunities(with communities: Set<Community>) {\n        self.communities = communities\n        \n        // Alphabetical\n        \n        var alphabeticSections: [String?: [Community]] = .init()\n        \n        let alphabeticSectionsGrouping: [String?: [Community]] = .init(\n            grouping: communities,\n            by: { alphabeticCategoryForCommunity($0) }\n        )\n        for section in alphabeticSectionsGrouping {\n            alphabeticSections[section.key] = section.value.sorted(by: self.sortPredicate)\n        }\n        \n        self.alphabeticSections = alphabeticSections\n        \n        // Instance\n        \n        var otherSection = [Community]()\n        let instanceSectionsGrouping: [String?: [Community]] = .init(grouping: communities, by: \\.host)\n        var instanceSections: [String?: [Community]] = .init()\n        \n        for section in instanceSectionsGrouping {\n            if section.value.count == 1, let community = section.value.first {\n                otherSection.append(community)\n            } else {\n                instanceSections[section.key] = section.value.sorted(by: self.sortPredicate)\n            }\n        }\n        if !otherSection.isEmpty {\n            instanceSections[nil] = otherSection.sorted(by: self.sortPredicate)\n        }\n        self.instanceSections = instanceSections\n        \n        favorites = communities.filter { favoriteIDs.contains($0.id) }\n    }\n    \n    func updateCommunitySubscription(community: Community) {\n        guard hasLoaded, let subscription = community.subscription.value else { return }\n        if subscription.subscribed {\n            if !communities.contains(community) {\n                addCommunity(community: community)\n            }\n            if isFavorited(community) != community.shouldBeFavorited {\n                if community.shouldBeFavorited {\n                    favoriteIDs.insert(community.id)\n                    favorites.sortedInsert(community, by: self.sortPredicate)\n                } else {\n                    favoriteIDs.remove(community.id)\n                    favorites.removeFirst { $0 === community }\n                }\n            }\n        } else if communities.contains(community) {\n            removeCommunity(community: community)\n        }\n    }\n        \n    private func addCommunity(community: Community) {\n        communities.insert(community)\n        \n        let alphabeticCategory = alphabeticCategoryForCommunity(community)\n        if alphabeticSections.keys.contains(alphabeticCategory) {\n            alphabeticSections[alphabeticCategory]?.sortedInsert(community, by: self.sortPredicate)\n        } else {\n            alphabeticSections[alphabeticCategory] = [community]\n        }\n        \n        let hostCategoryExists = instanceSections.keys.contains(community.host)\n        let hostExists: Bool = (\n            hostCategoryExists || instanceSections[nil, default: []].contains(where: { $0.host == community.host })\n        )\n        \n        if hostExists {\n            if hostCategoryExists {\n                instanceSections[community.host]?.sortedInsert(community, by: self.sortPredicate)\n            } else {\n                if let otherCommunity = instanceSections[nil]?.removeFirst(where: { $0.host == community.host }) {\n                    instanceSections[community.host] = [community, otherCommunity].sorted(by: self.sortPredicate)\n                } else {\n                    instanceSections[nil, default: []].append(community)\n                }\n            }\n        }\n    }\n    \n    private func removeCommunity(community: Community) {\n        communities.remove(community)\n        favoriteIDs.remove(community.id)\n        favorites.removeFirst { $0 === community }\n        let category = alphabeticCategoryForCommunity(community)\n        alphabeticSections[category]?.removeFirst { $0 === community }\n        if alphabeticSections[category]?.isEmpty ?? false {\n            alphabeticSections.removeValue(forKey: category)\n        }\n        \n        if var items = instanceSections[community.host] {\n            switch items.count {\n            case 1:\n                instanceSections.removeValue(forKey: community.host)\n                // Instance sections must contain at least two communities. If there is only one, it goes in\n                // the // \"other\" section instead. If we're removing a community from an instance section of\n                // size 2, we therefore need to move the remaining community to the \"other\" section.\n            case 2:\n                items.removeFirst { $0 === community }\n                instanceSections[nil, default: []].sortedInsert(items[0], by: self.sortPredicate)\n                instanceSections.removeValue(forKey: community.host)\n            default:\n                instanceSections[community.host]?.removeFirst { $0 === community }\n            }\n        } else {\n            alphabeticSections[nil]?.removeFirst { $0 === community }\n        }\n    }\n\n    private func sortPredicate(_ first: Community, _ second: Community) -> Bool {\n        let result = first.name.localizedCompare(second.name)\n        return switch result {\n        case .orderedAscending: true\n        case .orderedDescending: false\n        case .orderedSame: first.host.localizedCompare(second.host) == .orderedAscending\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/SubscriptionModel.swift",
    "content": "//\n//  SubscriptionModel.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 08/08/2024.\n//\n\nimport Foundation\n\npublic struct SubscriptionModel: Hashable, MergeableValue {\n    // These are the values actually provided by the API.\n    var actualTotal: Int\n    var actualLocal: Int?\n    \n    public var subscribed: Bool\n    \n    // When you subscribe, your instance asks the community host to confirm the subscription.\n    // Until a confirmation is received from the host, the subscription state is\n    // `LemmySubscribedType.pending`. The subscription count of the community doesn't change\n    // until the subscription status is confirmed by the community host. There also appears\n    // to exist a \"pending\" state for unsubscribing, but the API doesn't tell us when it's\n    // in this state.\n    //\n    // This property is \"true\" when the subscription is thought to be pending in **either**\n    // direction. Because we don't actually know whether an unsubscription is pending, this\n    // may not always be accurate.\n    var pending: Bool\n    \n    // This accounts for the `actualTotal` not taking your own pending subscription into account.\n    public var total: Int { actualTotal + pendingSubscriptionValue }\n    \n    // This accounts for the `actualLocal` not taking your own pending subscription into account.\n    /// Added in 0.19.4.\n    public var local: Int? {\n        guard let actualLocal else { return nil }\n        return actualLocal + pendingSubscriptionValue\n    }\n    \n    public func merge(with other: SubscriptionModel, using mergeType: ValueMergeType) -> SubscriptionModel {\n        switch mergeType {\n        case .disjunctive:\n            .init(\n                total: max(total, other.total),\n                local: nil,\n                subscribed: subscribed || other.subscribed,\n                pending: pending || other.pending\n            )\n        case .conjunctive:\n            .init(\n                total: max(total, other.total),\n                local: nil,\n                subscribed: subscribed && other.subscribed,\n                pending: pending && other.pending\n            )\n        }\n    }\n    \n    private var pendingSubscriptionValue: Int {\n        switch (subscribed, pending) {\n        case (true, true):\n            return 1\n        case (false, true):\n            return -1\n        case (_, false):\n            return 0\n        }\n    }\n\n    init(total: Int, local: Int?, subscribed: Bool, pending: Bool) {\n        self.actualTotal = total\n        self.actualLocal = local\n        self.subscribed = subscribed\n        self.pending = pending\n    }\n    \n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(actualTotal)\n        hasher.combine(actualLocal)\n        hasher.combine(subscribed)\n        hasher.combine(pending)\n    }\n    \n    public static func == (lhs: SubscriptionModel, rhs: SubscriptionModel) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n\nextension SubscriptionModel {\n    var subscribedType: LemmySubscribedType {\n        if subscribed {\n            pending ? .pending : .subscribed\n        } else {\n            .notSubscribed\n        }\n    }\n    \n    func withSubscriptionStatus(subscribed shouldSubscribe: Bool, isLocal: Bool) -> SubscriptionModel {\n        guard shouldSubscribe != subscribed else { return self }\n        \n        let diff: Int\n        if isLocal {\n            diff = shouldSubscribe ? 1 : -1\n        } else {\n            diff = 0\n        }\n        \n        let newLocal: Int?\n        if let actualLocal {\n            newLocal = actualLocal + diff\n        } else {\n            newLocal = nil\n        }\n        \n        return SubscriptionModel(\n            total: actualTotal + diff,\n            local: newLocal,\n            subscribed: shouldSubscribe,\n            pending: !(pending || isLocal)\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/UnreadCount.swift",
    "content": "//\n//  UnreadCount.swift\n//\n//\n//  Created by Sjmarf on 05/07/2024.\n//\n\nimport Foundation\nimport os\n\n@Observable\npublic final class UnreadCount {\n    let log: Logger = .mlemLogger()\n    \n    public let api: ApiClient\n    \n    var verifiedCount: [InboxItemType: Int] = .init()\n    var unverifiedCount: [InboxItemType: Int] = .init()\n    \n    public var replies: Int { self[.reply] }\n    public var mentions: Int { self[.mention] }\n    public var messages: Int { self[.message] }\n    public var postReports: Int { self[.postReport] }\n    public var commentReports: Int { self[.commentReport] }\n    public var messageReports: Int { self[.messageReport] }\n    public var registrationApplications: Int { self[.registrationApplication] }\n    \n    /// This value is incremented whenever the inbox count changes due to an\n    /// updated unread count being fetched from the API. It is not incremented when\n    /// state-faking is performed. This can be used as a trigger to decide when to\n    /// refresh the inbox.\n    public private(set) var refreshNumber: UInt = 0\n    \n    public var personalTotal: Int { replies + mentions + messages }\n    public var reportTotal: Int { postReports + commentReports + messageReports }\n    public var moderationTotal: Int { reportTotal + registrationApplications }\n    public var total: Int { personalTotal + moderationTotal }\n    \n    init(api: ApiClient) {\n        self.api = api\n    }\n    \n    @MainActor\n    func update(with newValues: [InboxItemType: Int]) {\n        var shouldUpdate = false\n        for (type, value) in newValues {\n            if verifiedCount[type] != value {\n                verifiedCount[type] = value\n                shouldUpdate = true\n            }\n        }\n        if shouldUpdate {\n            refreshNumber += 1\n        }\n    }\n    \n    @MainActor\n    func update(with sources: [any DictionaryConvertible]) {\n        update(\n            with: sources.reduce(into: [InboxItemType: Int]()) {\n                $0.merge($1.unreadCountDictionary) { $1 }\n            }\n        )\n    }\n    \n    func clear() {\n        verifiedCount = .init()\n        unverifiedCount = .init()\n    }\n    \n    func clear(_ types: Set<InboxItemType>) {\n        for type in types {\n            verifiedCount[type] = 0\n            unverifiedCount[type] = 0\n        }\n    }\n    \n    func updateUnverifiedItem(itemType: InboxItemType, isRead: Bool) {\n        let diff = isRead ? -1 : 1\n        unverifiedCount[itemType, default: 0] += diff\n    }\n    \n    func verifyItem(itemType: InboxItemType, isRead: Bool) {\n        let diff = isRead ? -1 : 1\n        verifiedCount[itemType, default: 0] += diff\n        unverifiedCount[itemType, default: 0] -= diff\n    }\n    \n    public subscript(_ type: InboxItemType) -> Int {\n        (verifiedCount[type] ?? 0) + (unverifiedCount[type] ?? 0)\n    }\n    \n    // If `alwaysMakeCalls` is `false`, `UnreadCount` will avoid making calls it doesn't need to (e.g. checking for\n    // moderation notifications if the user does not moderate any communities). You might want to set this to\n    // `true` if you are using this function to measure the response time of the server.\n    public func refresh(alwaysMakeCalls: Bool = false) async throws {\n        let values: [InboxItemType: Int] = try await withThrowingTaskGroup(\n            of: [InboxItemType: Int].self,\n            returning: [InboxItemType: Int].self\n        ) { taskGroup in\n            taskGroup.addTask {\n                try await self.api.repository.getPersonalUnreadCount().unreadCountDictionary\n            }\n            if !alwaysMakeCalls, self.api.username != nil, self.api.myPerson == nil || self.api.myInstance == nil {\n                // The theoretical solution to this is to store the moderated\n                // community IDs in `ApiClient.Context` and `await` them here.\n                log.warning(\"ApiClient.myPerson or ApiClient.myInstance is nil at UnreadCount refresh - this may lead to unneeded API calls\")\n            }\n            \n            if try await self.api.supports(.viewReports) {\n                if alwaysMakeCalls || !(self.api.myPerson?.moderatedCommunities.value_?.isEmpty ?? false) || self.api.isAdmin {\n                    taskGroup.addTask {\n                        do {\n                            return try await self.api.repository.getReportCount(communityId: nil).unreadCountDictionary\n                        } catch let ApiClientError.response(response, _) where response.notModOrAdmin {\n                            return [:]\n                        }\n                    }\n                }\n                // Don't use `api.isAdmin` here; it falls back to `false` and we need to fallback to `true`\n                if alwaysMakeCalls || api.myInstance?.administrators.value?.contains(where: { $0.id == api.myPerson?.id }) ?? true {\n                    taskGroup.addTask {\n                        do {\n                            return try await [.registrationApplication: self.api.getRegistrationApplicationCount()]\n                        } catch let ApiClientError.response(response, _) where response.notAdmin {\n                            return [:]\n                        }\n                    }\n                }\n            }\n            return try await taskGroup.reduce(into: [:]) { $0.merge($1) { $1 } }\n        }\n        await update(with: values)\n    }\n}\n\npublic enum InboxItemType: Codable {\n    case reply, mention, message\n    case postReport, commentReport, messageReport, registrationApplication\n}\n\npublic extension Set<InboxItemType> {\n    static var all: Set<InboxItemType> {\n        [.reply, .mention, .message, .postReport, .commentReport, .messageReport, .registrationApplication]\n    }\n    \n    static var personal: Set<InboxItemType> {\n        [.reply, .mention, .message]\n    }\n    \n    static var reports: Set<InboxItemType> {\n        [.postReport, .commentReport, .messageReport]\n    }\n    \n    static var moderatorAndAdmin: Set<InboxItemType> {\n        reports.union([.registrationApplication])\n    }\n}\n\nextension UnreadCount {\n    protocol DictionaryConvertible {\n        var unreadCountDictionary: [InboxItemType: Int] { get }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/UpdateQueues/InboxNotificationUpdateQueue.swift",
    "content": "//\n//  InboxNotificationUpdateQueue.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-04.\n//\n\nimport os\nimport Semaphore\n\n/// This actor synchronizes state updates for a particular inbox notification.\n///\n/// Calls are queued using `addItem`, and each call must return an `InboxNotificationSnapshot`. When each call returns, `lastVerifiedSnapshot` is updated\n/// with the returned snapshot. Each call will only execute when the previous one finishes, ensuring that `lastVerifiedSnapshot` always accurately reflects\n/// the most recently queried server state.\n///\n/// When the queue finishes executing altogether, it updates its parent model with the most recent snapshot. State faking is performed by the model\n/// before the work item is queued.\n///\npublic actor InboxNotificationUpdateQueue {\n    let log: Logger = .mlemLogger()\n    \n    weak var parent: InboxNotification?\n    \n    private var lastVerifiedSnapshot: InboxNotificationSnapshot?\n    \n    private var semaphore: AsyncSemaphore = .init(value: 1)\n    private var queue: Queue<InboxNotificationUpdateTask> = .init()\n\n    func setParent(_ newParent: InboxNotification) {\n        parent = newParent\n        lastVerifiedSnapshot = newParent.takeSnapshot()\n    }\n    \n    /// Add a task to the queue for a repository call that returns a complete snapshot.\n    /// - Note: prefer this method over the snapshot-modifying variant below\n    func addItem(item: @escaping () async throws -> InboxNotificationSnapshot) {\n        addItem(.createsSnapshot(item))\n    }\n    \n    /// Add a task to the queue for a repository call that **does not** return a complete snapshot. The queue will provide the latest verified\n    /// snapshot to the task, which should then modify and return the snapshot according to the repository call result.\n    /// - Note: **only** use this method when absolutely necessary; if the repository returns a complete snapshot, use the variant above.\n    func addItem(item: @escaping (InboxNotificationSnapshot) async throws -> InboxNotificationSnapshot) {\n        addItem(.modifiesSnapshot(item))\n    }\n    \n    private func addItem(_ item: InboxNotificationUpdateTask) {\n        queue.enqueue(item)\n        if queue.numItems == 1 {\n            Task {\n                await executeQueue()\n            }\n        }\n    }\n    \n    /// Attempts to update the notification with the given snapshot. If any tasks are queued, no action will be taken.\n    /// This method should be called when new snapshots are received by actions in a foreign object's queue or by headless calls\n    func attemptDirectUpdate(with snapshot: InboxNotificationSnapshot) async {\n        guard queue.numItems == 0, let parent else { return }\n        await updateParent(parent, with: snapshot, isResultOfTask: false)\n    }\n    \n    private func executeQueue() async {\n        await semaphore.wait()\n        defer {\n            semaphore.signal()\n            log.debug(\"Finished executing queue\")\n        }\n        log.info(\"Executing queue\")\n        \n        guard let parent else {\n            assertionFailure(\"Cannot execute queue with no parent!\")\n            return\n        }\n        // this shouldn't be possible, since lastVerifiedSnapshot is set when the parent is set\n        guard var lastVerifiedSnapshot else {\n            assertionFailure(\"Cannot execute queue with no lastVerifiedSnapshot!\")\n            return\n        }\n        while let task = queue.next() {\n            log.debug(\"Found next task\")\n            do {\n                let snapshot: InboxNotificationSnapshot\n                switch task {\n                case let .createsSnapshot(callback):\n                    snapshot = try await callback()\n                case let .modifiesSnapshot(callback):\n                    snapshot = try await callback(lastVerifiedSnapshot)\n                }\n                \n                self.lastVerifiedSnapshot = snapshot\n                lastVerifiedSnapshot = snapshot // also need to update scoped lastVerifiedSnapshot so updateParent gets the correct value\\\n            } catch {\n                log.error(\"\\(error.localizedDescription)\")\n            }\n            queue.dequeue()\n        }\n        \n        await updateParent(parent, with: lastVerifiedSnapshot, isResultOfTask: true)\n    }\n    \n    private func updateParent(\n        _ parent: InboxNotification,\n        with snapshot: InboxNotificationSnapshot,\n        isResultOfTask: Bool\n    ) async {\n        await parent.snapshotUpdate(with: snapshot, isResultOfTask: isResultOfTask)\n    }\n}\n\nenum InboxNotificationUpdateTask {\n    case createsSnapshot(() async throws -> InboxNotificationSnapshot)\n    case modifiesSnapshot((InboxNotificationSnapshot) async throws -> InboxNotificationSnapshot)\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/UpdateQueues/Queue.swift",
    "content": "//\n//  Queue.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-04.\n//\n\npublic class Queue<T> {\n    private var items: [T] = .init()\n    \n    var numItems: Int { items.count }\n    \n    func enqueue(_ item: T) {\n        items.append(item)\n    }\n    \n    @discardableResult\n    func dequeue() -> T? {\n        guard !items.isEmpty else { return nil }\n        return items.removeFirst()\n    }\n    \n    func next() -> T? { items.first }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/UpdateQueues/UnifiedUpdateQueue.swift",
    "content": "//\n//  UnifiedUpdateQueue.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-12-27.\n//\n\nimport os\nimport Semaphore\n\n/// This actor synchronizes state updates for a particular post.\n///\n/// Calls are queued using `addItem`, and each call must return a `PostSnapshotProviding`. When each call returns, `lastVerifiedSnapshot` is updated\n/// with the returned snapshot. Each call will only execute when the previous one finishes, ensuring that `lastVerifiedSnapshot` always accurately reflects\n/// the most recently queried server state.\n///\n/// When the queue finishes executing altogether, it updates its parent model with the most recent snapshot. State faking is performed by the model\n/// before the work item is queued.\n///\n/// - Note: some care must be taken to ensure that `parent` always points to a valid model. When a model is initialized, it updates `parent` to be itself;\n/// likewise, when a model is deinitialized, it updates `parent` to be the next lower-tier model contained within itself (e.g., `Post3`'s deinit updates parent\n/// to be `post2`). If this update is not performed, `parent` may become nil and the queue will refuse to execute. In debug mode this will throw an error,\n/// while in production the queue will simply not run until an item is added when the parent is present.\npublic actor UnifiedUpdateQueue<Model: UnifiedModelProviding> {\n    let log: Logger = .mlemLogger()\n    \n    let parent: Model\n    \n    private var lastVerifiedProperties: Model.Properties\n    private var upgraded: Bool = false\n    \n    private var semaphore: AsyncSemaphore = .init(value: 1)\n    private var queue: Queue<UpdateTask> = .init()\n    \n    init(parent: Model, properties: Model.Properties) {\n        self.parent = parent\n        self.lastVerifiedProperties = properties\n    }\n    \n    /// Updates parent with its highest tier information.\n    /// - Note: this function simply queues the upgrade and returns very quickly. For use with interactive spinners, use `refresh()`\n    func upgrade() async throws {\n        // this method is a unique case because upgrade will be called at every property access on the parent model until\n        // the required properties are provided. Therefore we only allow a single call\n        guard !upgraded else {\n            return\n        }\n        upgraded = true\n        \n        addItem {\n            return try await self.parent.fetchUpgraded()\n        }\n    }\n    \n    /// Updates parent with its highest tier information. Only returns when the upgrade is complete.\n    func refresh() async throws {\n        await semaphore.wait()\n        defer {\n            semaphore.signal()\n            log.debug(\"Finished refresh\")\n        }\n        \n        let newProperties = try await self.parent.fetchUpgraded()\n        self.lastVerifiedProperties.merge(newProperties)\n        await updateParent(parent, with: lastVerifiedProperties)\n    }\n    \n    /// Add a task to the queue for a repository call that returns a complete snapshot.\n    /// - Note: prefer this method over the snapshot-modifying variant below\n    func addItem(item: @escaping () async throws -> Model.Properties) {\n        addItem(.createsProperties(item))\n    }\n    \n    /// Add a task to the queue for a repository call that **does not** return a complete snapshot. The queue will provide the latest verified\n    /// snapshot to the task, which should then modify and return the snapshot according to the repository call result.\n    /// - Note: **only** use this method when absolutely necessary; if the repository returns a complete snapshot, use the variant above.\n    func addItem(item: @escaping (Model.Properties) async throws -> Model.Properties) {\n        addItem(.modifiesProperties(item))\n    }\n    \n    /// Attempts to update the post with the given snapshot. If any tasks are queued, no action will be taken.\n    /// This method should be called when new snapshots are received by actions in a foreign object's queue or by headless calls\n    func attemptDirectUpdate(with properties: Model.Properties) async {\n        guard queue.numItems == 0 else { return }\n        await updateParent(parent, with: properties)\n    }\n    \n    private func addItem(_ item: UpdateTask) {\n        queue.enqueue(item)\n        if queue.numItems == 1 {\n            Task {\n                await executeQueue()\n            }\n        }\n    }\n    \n    private func executeQueue() async {\n        await semaphore.wait()\n        defer {\n            semaphore.signal()\n            log.debug(\"Finished executing queue\")\n        }\n        log.info(\"Executing queue\")\n        \n        while let task = queue.next() {\n            log.debug(\"Found next task\")\n            do {\n                let newProperties: Model.Properties\n                switch task {\n                case let .createsProperties(callback):\n                    newProperties = try await callback()\n                case let .modifiesProperties(callback):\n                    newProperties = try await callback(lastVerifiedProperties)\n                }\n                \n                self.lastVerifiedProperties.merge(newProperties)\n            } catch {\n                log.error(\"\\(error.localizedDescription)\")\n            }\n            queue.dequeue()\n        }\n        \n        await updateParent(parent, with: lastVerifiedProperties)\n    }\n    \n    @MainActor\n    private func updateParent(_ parent: Model, with properties: Model.Properties) {\n         // parent must be passed in rather than accessed directly due to actor access constraints\n        parent.update(with: properties)\n    }\n    \n    enum UpdateTask {\n        case createsProperties(() async throws -> Model.Properties)\n        case modifiesProperties((Model.Properties) async throws -> Model.Properties)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/UpdateQueues/UpdateStatus.swift",
    "content": "//\n//  UpdateStatus.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-25.\n//\n\npublic enum UpdateStatus {\n    case success\n    case failure(Error)\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Content Models/VotesModel.swift",
    "content": "//\n//  VotesModel.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-08-26.\n//\n\nimport Foundation\n\npublic struct VotesModel: Hashable, Equatable {\n    public var total: Int { upvotes - downvotes }\n    public var upvotes: Int\n    public var downvotes: Int\n    public var myVote: ScoringOperation\n\n    // init from API type\n    public init(from voteCount: any ApiContentAggregatesProtocol, myVote: ScoringOperation?) {\n        self.upvotes = voteCount.upvotes\n        self.downvotes = voteCount.downvotes\n        self.myVote = myVote ?? .none\n    }\n\n    // raw init\n    public init(upvotes: Int, downvotes: Int, myVote: ScoringOperation) {\n        self.upvotes = upvotes\n        self.downvotes = downvotes\n        self.myVote = myVote\n    }\n\n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(upvotes)\n        hasher.combine(downvotes)\n        hasher.combine(myVote)\n    }\n    \n    public static func == (lhs: VotesModel, rhs: VotesModel) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n\nextension VotesModel {\n    /// Returns the result of applying the given scoring operation. Assumes that it is a valid operation (i.e., not upvoting an upvoted post or downvoting a downvoted one)\n    /// - Parameter operation: operation to apply\n    /// - Returns: VotesModel representing the result of applying the given operation\n    func applyScoringOperation(operation: ScoringOperation) -> VotesModel {\n        assert(!(operation == .upvote && myVote == .upvote), \"Cannot apply upvote to upvoted score\")\n        assert(!(operation == .downvote && myVote == .downvote), \"Cannot apply downvote to downvoted score\")\n\n        var upvoteDelta: Int\n        var downvoteDelta: Int\n\n        switch myVote {\n        case .upvote:\n            // no matter what, removing 1 upvote; if downvoting, adding 1 downvote\n            upvoteDelta = -1\n            downvoteDelta = operation == .downvote ? 1 : 0\n        case .none:\n            // adding 1 to whichever operation we get as long as it's not resetVote\n            upvoteDelta = operation == .upvote ? 1 : 0\n            downvoteDelta = operation == .downvote ? 1 : 0\n        case .downvote:\n            // no matter what, removing 1 downvote; if upvoting, adding 1 upvote\n            upvoteDelta = operation == .upvote ? 1 : 0\n            downvoteDelta = -1\n        }\n\n        return VotesModel(\n            upvotes: upvotes + upvoteDelta,\n            downvotes: downvotes + downvoteDelta,\n            myVote: operation\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/ApiPictrsFile.swift",
    "content": "//\n//  LemmyPictrsFile.swift\n//\n//\n//  Created by Sjmarf on 26/08/2024.\n//\n\nimport Foundation\n\npublic struct LemmyPictrsFile: Codable, Equatable {\n    public let file: String\n    public let deleteToken: String\n}\n\npublic extension LemmyPictrsFile {\n    enum CodingKeys: String, CodingKey {\n        case file\n        case deleteToken = \"delete_token\"\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/ApiPictrsUploadResponse.swift",
    "content": "//\n//  LemmyPictrsUploadResponse.swift\n//\n//\n//  Created by Sjmarf on 26/08/2024.\n//\n\nimport Foundation\n\npublic struct LemmyPictrsUploadResponse: Codable {\n    public let msg: String?\n    public let files: [LemmyPictrsFile]?\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/APIComment+Extensions.swift",
    "content": "//\n//  LemmyComment+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-02-19.\n//\n\nimport Foundation\n\nextension LemmyComment: CacheIdentifiable {\n    public var cacheId: Int { id }\n    \n    public var parentId: Int? {\n        let components = path.components(separatedBy: \".\")\n\n        guard path != \"0\", components.count != 2 else {\n            return nil\n        }\n\n        guard let id = components.dropLast(1).last else {\n            return nil\n        }\n\n        return Int(id)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/APIPost+Extensions.swift",
    "content": "//\n//  LemmyPost+ActorIdentifiable.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-02-19.\n//\n\nimport Foundation\n\nextension LemmyPost {\n    var linkUrl: URL? { LemmyURL(string: url?.absoluteString ?? \"\")?.url }\n    // var thumbnailImageUrl: URL? { LemmyURL(string: thumbnail_url)?.url }\n    var thumbnailImageUrl: URL? { thumbnailUrl }\n    \n    var embed: PostEmbed? {\n        if embedTitle != nil || embedDescription != nil || embedVideoUrl != nil {\n            return .init(\n                title: embedTitle,\n                description: embedDescription,\n                videoUrl: embedVideoUrl\n            )\n        }\n        return nil\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/APISubscribedType+Extensions.swift",
    "content": "//\n//  LemmySubscribedType+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-02-19.\n//\n\nimport Foundation\n\npublic extension LemmySubscribedType {\n    var isSubscribed: Bool { self != .notSubscribed }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/ApiCommentAggregates+Extensions.swift",
    "content": "//\n//  LemmyCommentAggregates.swift\n//\n//\n//  Created by Sjmarf on 24/06/2024.\n//\n\nimport Foundation\n\nextension LemmyCommentAggregates: ApiContentAggregatesProtocol {\n    public var comments: Int { childCount }\n    \n    static var zero: Self {\n        .init(commentId: 0, score: 0, upvotes: 0, downvotes: 0, published: .distantPast, childCount: 0)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/ApiCommunityAggregates+Extensions.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-22.\n//\n\nimport Foundation\n\nextension LemmyCommunityAggregates {\n    static var zero: Self {\n        .init(\n            communityId: 0,\n            subscribers: 0,\n            posts: 0,\n            comments: 0,\n            published: .distantPast,\n            usersActiveDay: 0,\n            usersActiveWeek: 0,\n            usersActiveMonth: 0,\n            usersActiveHalfYear: 0,\n            subscribersLocal: 0\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/ApiCommunityFollowerState+Extensions.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-05.\n//\n\nimport Foundation\n\npublic extension LemmyCommunityFollowerState {\n    var isSubscribed: Bool { self == .accepted }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/ApiGetModlogResponse+Extensions.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-17.\n//\n\nimport Foundation\n\npublic extension LemmyGetModlogResponse {\n    func toSnapshots() throws(ApiClientError) -> [ModlogEntrySnapshot] {\n        var result = try (removedPosts).map(ModlogEntrySnapshot.init)\n        result += try (lockedPosts).map(ModlogEntrySnapshot.init)\n        result += try (featuredPosts).map(ModlogEntrySnapshot.init)\n        result += try (removedComments).map(ModlogEntrySnapshot.init)\n        result += try (removedCommunities).map(ModlogEntrySnapshot.init)\n        result += try (bannedFromCommunity).map(ModlogEntrySnapshot.init)\n        result += try (banned).map(ModlogEntrySnapshot.init)\n        result += try (addedToCommunity).map(ModlogEntrySnapshot.init)\n        result += try (transferredToCommunity).map(ModlogEntrySnapshot.init)\n        result += try (added).map(ModlogEntrySnapshot.init)\n        result += try (adminPurgedPersons).map(ModlogEntrySnapshot.init)\n        result += try (adminPurgedCommunities).map(ModlogEntrySnapshot.init)\n        result += try (adminPurgedPosts).map(ModlogEntrySnapshot.init)\n        result += try (adminPurgedComments).map(ModlogEntrySnapshot.init)\n        result += try (hiddenCommunities).map(ModlogEntrySnapshot.init)\n        return result\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/ApiPersonAggregates+Extensions.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-22.\n//\n\nimport Foundation\n\nextension LemmyPersonAggregates {\n    static var zero: Self {\n        .init(personId: 0, postCount: 0, commentCount: 0)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/ApiPostAggregates+Extensions.swift",
    "content": "//\n//  LemmyPostAggregates+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-03.\n//\n\nimport Foundation\n\nextension LemmyPostAggregates: ApiContentAggregatesProtocol {\n    static var zero: Self {\n        .init(\n            postId: 0,\n            comments: 0,\n            score: 0,\n            upvotes: 0,\n            downvotes: 0,\n            published: .distantPast,\n            newestCommentTime: nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/ApiPrivateMessageReportView+Extensions.swift",
    "content": "//\n//  LemmyPrivateMessageReportView+Extensions.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-16.\n//\n\nimport Foundation\n\nextension LemmyPrivateMessageReportView {\n    func toPrivateMessageView() -> LemmyPrivateMessageView {\n        .init(\n            privateMessage: privateMessage,\n            creator: privateMessageCreator,\n            recipient: creator // Only the recipient of the message can report it.\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/ApiSiteAggregates+Extensions.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-22.\n//\n\nimport Foundation\n\nextension LemmySiteAggregates {\n    static var zero: Self {\n        .init(\n            siteId: 0,\n            users: 0,\n            posts: 0,\n            comments: 0,\n            communities: 0,\n            usersActiveDay: 0,\n            usersActiveWeek: 0,\n            usersActiveMonth: 0,\n            usersActiveHalfYear: 0\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/PieFedCommentAggregates+Extensions.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-25.\n//\n\nimport Foundation\n\nextension PieFedCommentAggregates: ApiContentAggregatesProtocol {\n    public var comments: Int { childCount }\n    \n    static var zero: Self {\n        .init(commentId: 0, score: 0, upvotes: 0, downvotes: 0, published: .distantPast, childCount: 0)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/PieFedPostAggregates+Extensions.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-25.\n//\n\nimport Foundation\n\nextension PieFedPostAggregates: ApiContentAggregatesProtocol {\n    static var zero: Self {\n        .init(\n            postId: 0,\n            comments: 0,\n            score: 0,\n            upvotes: 0,\n            downvotes: 0,\n            published: .distantPast,\n            newestCommentTime: .distantPast,\n            crossPosts: 0\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/Extensions/PieFedSubscribedType+Extensions.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-19.\n//\n\nimport Foundation\n\npublic extension PieFedSubscribedType {\n    var isSubscribed: Bool { self != .notSubscribed }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Custom API Models/LemmyPagedResponse.swift",
    "content": "//\n//  LemmyPagedResponse.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-12-05.\n//  \n\nimport Foundation\n\npublic struct LemmyPagedResponse<Value: Codable>: Codable {\n    public let items: [Value]\n    public let nextPage: String?\n    public let prevPage: String?\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Extensions/Array+Extensions.swift",
    "content": "//\n//  Array+Prepend.swift\n//  Mlem\n//\n//  Created by David Bureš on 07.05.2023.\n//\n\nimport Foundation\n\npublic extension Array {\n    mutating func prepend(_ newElement: Element) {\n        insert(newElement, at: 0)\n    }\n    \n    mutating func sortedInsert(_ newElement: Element, by predicate: (Element, Element) -> Bool) {\n        insert(newElement, at: insertionIndex(for: { predicate($0, newElement) }))\n    }\n    \n    subscript(safeIndex index: Int) -> Element? {\n        guard index >= 0, index < endIndex else {\n            return nil\n        }\n        \n        return self[index]\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Extensions/Bool+Extensions.swift",
    "content": "//\n//  Bool+Extensions.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-05-05.\n//\n\nextension Bool {\n    public func or(_ other: Bool) -> Bool {\n        self || other\n    }\n}\n\nextension Bool: MergeableValue {\n    public func merge(with other: Bool, using mergeType: ValueMergeType) -> Bool {\n        switch mergeType {\n        case .disjunctive: self || other\n        case .conjunctive: self && other\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Extensions/InstanceSummarySoftware+Extensions.swift",
    "content": "//\n//  InstanceSummarySoftware+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2026-03-27.\n//\n\nimport MlemBackend\n\nextension InstanceSummarySoftware {\n    init(from software: SiteSoftware) {\n        let type: InstanceSummarySoftwareType = switch software.type {\n        case .lemmy: .lemmy\n        case .pieFed: .pieFed\n        }\n        \n        self.init(\n            type: type,\n            version: software.version.description\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Extensions/RandomAccessCollection+Extensions.swift",
    "content": "//\n//  RandomAccessCollection+Extensions.swift\n//\n//\n//  Created by Sjmarf on 05/05/2024.\n//\n\nimport Foundation\n\nextension RandomAccessCollection {\n    func insertionIndex(for predicate: (Element) -> Bool) -> Index {\n        var slice: SubSequence = self[...]\n\n        while !slice.isEmpty {\n            let middle = slice.index(slice.startIndex, offsetBy: slice.count / 2)\n            if predicate(slice[middle]) {\n                slice = slice[index(after: middle)...]\n            } else {\n                slice = slice[..<middle]\n            }\n        }\n        return slice.startIndex\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Extensions/RangeReplaceableCollection+Extensions.swift",
    "content": "//\n//  RangeReplaceableCollection+Extensions.swift\n//\n//\n//  Created by Sjmarf on 11/05/2024.\n//\n\nimport Foundation\n\nextension RangeReplaceableCollection {\n    @discardableResult\n    mutating func removeFirst(where predicate: (Element) throws -> Bool) rethrows -> Element? {\n        guard let index = try firstIndex(where: predicate) else { return nil }\n        return remove(at: index)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Extensions/String+Extensions.swift",
    "content": "//\n//  String+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-12-10.\n//\n\nimport Foundation\n\npublic extension String {\n    /// Returns true if the given array of strings contains any word which appears as a substring of this string\n    func isContainedIn(_ strings: [String]) -> Bool {\n        strings.contains { contains($0) }\n    }\n    \n    /// Returns true if the given set of strings contains any word which appears as a substring of this string\n    func isContainedIn(_ strings: Set<String>) -> Bool {\n        strings.contains { contains($0) }\n    }\n    \n    /// Returns true if this string contains:\n    /// - Any single word that matches a single word in filteredKeywords\n    /// - Any sequence of words that precisely matches a multi-word phrase in filteredKeywords\n    func failsKeywordFilter(keywords: Set<String>, phrases: Set<[String]>) -> Bool {\n        // split on any non-letter/number characters so \"keyword's\" is filtered as \"keyword\" \"s\"\n        let words = split(separator: /[^[:alnum:]]/)\n            .map { $0.lowercased() }\n        \n        var partialMatches: [PartialMatch] = .init()\n        for word in words {\n            // check single keywords\n            if keywords.contains(word) { return true }\n            \n            // check if any partial matches succeed\n            var matchedPhrase: Bool = false\n            partialMatches = partialMatches.filter { partial in\n                switch partial.matchNextWord(word) {\n                case .failed: return false\n                case .partial: return true\n                case .matched:\n                    matchedPhrase = true\n                    return true\n                }\n            }\n            if matchedPhrase { return true }\n            \n            // check if this word starts a new partial match\n            for phrase in phrases {\n                guard let firstWord = phrase.first else {\n                    assertionFailure(\"Invalid phrase (no first element)\")\n                    continue\n                }\n                if word == firstWord {\n                    partialMatches.append(.init(phrase: phrase))\n                }\n            }\n        }\n        \n        return false\n    }\n    \n    // Returns true if this string contains any literals in  the given set\n    func failsLiteralFilter(literals: Set<String>) -> Bool {\n        return literals.contains(where: { self.contains($0) })\n    }\n}\n\nprivate enum MatchState {\n    case partial, failed, matched\n}\n\nprivate class PartialMatch {\n    private let phrase: [String]\n    private var index: Int = 1 // starts at 1 because only initialized if first word matches\n    \n    init(phrase: [String]) {\n        assert(phrase.count > 0, \"Invalid phrase\")\n        self.phrase = phrase\n    }\n    \n    func matchNextWord(_ word: String) -> MatchState {\n        guard let nextWord = phrase[safeIndex: index] else {\n            assertionFailure(\"No next word!\")\n            return .failed\n        }\n        if word == nextWord {\n            if index == phrase.count - 1 {\n                return .matched\n            } else {\n                index += 1\n                return .partial\n            }\n        }\n        return .failed\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Extensions/URL+Extensions.swift",
    "content": "//\n//  URL+Identifiable.swift\n//  Mlem\n//\n//  Created by Nicholas Lawson on 04/06/2023.\n//\n\nimport Foundation\nimport os\n\nextension URL: @retroactive Identifiable {\n    public var id: URL { absoluteURL }\n}\n\npublic extension URL {\n    static func post(host: String, id: Int) -> Self {\n        var components = URLComponents()\n        components.scheme = \"https\"\n        components.host = host\n        components.path = \"/post/\\(id)\"\n        return components.url! // This will always succeed\n    }\n    \n    static func comment(host: String, id: Int) -> Self {\n        var components = URLComponents()\n        components.scheme = \"https\"\n        components.host = host\n        components.path = \"/comment/\\(id)\"\n        return components.url! // This will always succeed\n    }\n    \n    static func community(host: String, name: String) -> Self {\n        var components = URLComponents()\n        components.scheme = \"https\"\n        components.host = host\n        components.path = \"/c/\\(name)\"\n        return components.url! // This will always succeed\n    }\n\n    static func person(host: String, name: String) -> Self {\n        var components = URLComponents()\n        components.scheme = \"https\"\n        components.host = host\n        components.path = \"/u/\\(name)\"\n        return components.url! // This will always succeed\n    }\n}\n\npublic extension URL {\n    // Spec described here: https://join-lemmy.org/docs/contributors/04-api.html#images\n    func withIconSize(_ size: Int?) -> URL {\n        guard scheme == \"http\" || scheme == \"https\" else { return self }\n        guard let size else { return self }\n        guard var components = URLComponents(url: self, resolvingAgainstBaseURL: true) else {\n            Logger.universal.warning(\"Failed to create URLComponents\")\n            return appending(queryItems: [.init(name: \"thumbnail\", value: String(size))])\n        }\n        var queryItems = components.queryItems ?? []\n        queryItems.removeFirst(where: { $0.name == \"thumbnail\" })\n        queryItems.append(.init(name: \"thumbnail\", value: String(size)))\n        components.queryItems = queryItems\n        return components.url ?? self\n    }\n    \n    func removingPathComponents() -> URL {\n        var components = URLComponents()\n        components.scheme = scheme\n        components.host = host\n        components.port = port\n        return components.url!\n    }\n    \n    private struct LoopsVideoResponse: Codable {\n        let data: Body\n        internal struct Body: Codable {\n            let media: Media\n            internal struct Media: Codable {\n                let src_url: URL\n            }\n        }\n    }\n    \n    /// Attempts to extract the underlying loops.video media URL from this URL\n    /// - Returns: loops.video media URL if this is a loops.video url and the underlying URL was successfully parsed, nil otherwise\n    func parseEmbeddedLoops() async -> URL? {\n        // TODO: Pending loops.video maturation\n        // - More reliable way of determining if this is a Loops server\n        // - More robust way of extracting media URL (preferably API support)\n        guard host() == \"loops.video\" else { return nil }\n        \n        do {\n            let (websiteContent, _) = try await URLSession.shared.data(from: self)\n            \n            // parse video API ID from website content\n            let apiIdRegex = /<meta property=\"og:image\" content=\"https:\\/\\/loopsusercontent\\.com\\/videos\\/\\d+\\/((?<apiId>\\d*))\\/.*\\/>/\n            guard let str: String = String(data: websiteContent, encoding: .utf8),\n                  let match = str.firstMatch(of: apiIdRegex),\n                  let apiUrl = URL(string: \"https://loops.video/api/v1/video/\\(match.apiId)\") else {\n                return nil\n            }\n            \n            // query API for video id\n            let (apiResponse, _) = try await URLSession.shared.data(from: apiUrl)\n            let decodedResponse = try JSONDecoder.defaultDecoder.decode(LoopsVideoResponse.self, from: apiResponse)\n            return decodedResponse.data.media.src_url\n        } catch {\n            Logger.universal.error(\"Failed to parse embedded loops: \\(error.localizedDescription)\")\n        }\n        return nil\n    }\n    \n    /// Path extension of this URL, taking into account image proxy behavior\n    var proxyAwarePathExtension: String? {\n        var ret = pathExtension\n        \n        // image proxies that use url query param don't have pathExtension so we extract it from the embedded url\n        if ret.isEmpty,\n           let components = URLComponents(url: self, resolvingAgainstBaseURL: true),\n           let queryItems = components.queryItems,\n           let baseUrlString = queryItems.first(where: { $0.name == \"url\" })?.value,\n           let baseUrl = URL(string: baseUrlString) {\n            ret = baseUrl.pathExtension\n        }\n        \n        return ret.isEmpty ? nil : ret.lowercased()\n    }\n    \n    var isMedia: Bool {\n        if scheme == \"mlempreview\" { return true }\n        return proxyAwarePathExtension?.isContainedIn([\"jpg\", \"jpeg\", \"png\", \"webp\", \"gif\", \"avif\", \"mp4\"]) ?? false\n    }\n    \n    var isYouTubeLink: Bool {\n        guard let host = host()?.lowercased() else { return false }\n        return host == \"youtube.com\" || host == \"www.youtube.com\" || host == \"youtu.be\" || host == \"m.youtube.com\"\n    }\n    \n    var youTubeVideoId: String? {\n        guard isYouTubeLink else { return nil }\n        \n        let host = host()?.lowercased() ?? \"\"\n        \n        if host == \"youtu.be\" {\n            let pathComponents = pathComponents\n            if pathComponents.count > 1 {\n                return pathComponents[1]\n            }\n        } else if host == \"youtube.com\" || host == \"www.youtube.com\" || host == \"m.youtube.com\" {\n            guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true),\n                  let queryItems = components.queryItems,\n                  let videoId = queryItems.first(where: { $0.name == \"v\" })?.value else {\n                let pathComponents = pathComponents\n                if pathComponents.count > 2, pathComponents[1] == \"embed\" {\n                    return pathComponents[2]\n                }\n                \n                return nil\n            }\n            \n            return videoId\n        }\n        \n        return nil\n    }\n    \n    var youTubeThumbnailUrl: URL? {\n        guard let videoId = youTubeVideoId else { return nil }\n        return URL(string: \"https://img.youtube.com/vi/\\(videoId)/mqdefault.jpg\")\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Comment/SearchCommentFeedLoader.swift",
    "content": "//\n//  SearchCommentFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-01-18.\n//\n\nimport Foundation\n\n@Observable\npublic class SearchCommentFetcher: Fetcher<Comment> {\n    public enum SortType {\n        case v4(SearchSortType)\n        case v3(CommentSortType)\n    }\n    \n    public var query: String\n    public var listing: ListingType\n    public var sort: SortType\n    public var communityId: Int?\n    public var creatorId: Int?\n    \n    init(\n        api: ApiClient,\n        query: String,\n        communityId: Int?,\n        creatorId: Int?,\n        pageSize: Int,\n        listing: ListingType,\n        sort: SortType\n    ) {\n        self.query = query\n        self.communityId = communityId\n        self.creatorId = creatorId\n        self.listing = listing\n        self.sort = sort\n        \n        super.init(api: api, pageSize: pageSize)\n    }\n    \n    override func fetchPage(_ page: Int) async throws -> FetchResponse {\n        let comments: [Comment]\n        switch sort {\n        case let .v4(searchSortType):\n            comments = try await api.searchComments(\n                query: query,\n                page: page,\n                limit: pageSize,\n                communityId: communityId,\n                creatorId: creatorId,\n                filter: listing,\n                sort: searchSortType\n            )\n        case let .v3(commentSortType):\n            comments = try await api.searchComments(\n                query: query,\n                page: page,\n                limit: pageSize,\n                communityId: communityId,\n                creatorId: creatorId,\n                filter: listing,\n                sort: commentSortType\n            )\n        }\n        \n        return .init(\n            items: comments,\n            prevCursor: nil,\n            nextCursor: nil\n        )\n    }\n    \n    public func changeApi(to newApi: ApiClient) async {\n        await super.changeApi(to: newApi, context: .none())\n    }\n}\n\n@Observable\npublic class SearchCommentFeedLoader: StandardFeedLoader<Comment> {\n    public var api: ApiClient\n    \n    // force unwrap because this should ALWAYS be a SearchCommentFetcher\n    public var searchCommentFetcher: SearchCommentFetcher { fetcher as! SearchCommentFetcher }\n    \n    public init(\n        api: ApiClient,\n        query: String = \"\",\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        pageSize: Int = 20,\n        listing: ListingType = .all,\n        sort: SearchCommentFetcher.SortType = .v4(.top(.allTime))\n    ) {\n        self.api = api\n\n        super.init(\n            filter: .init(),\n            fetcher: SearchCommentFetcher(\n                api: api,\n                query: query,\n                communityId: communityId,\n                creatorId: creatorId,\n                pageSize: pageSize,\n                listing: listing,\n                sort: sort\n            )\n        )\n    }\n    \n    public func refresh(\n        query: String? = nil,\n        listing: ListingType? = nil,\n        sort: SearchCommentFetcher.SortType? = nil,\n        clearBeforeRefresh: Bool = false\n    ) async throws {\n        searchCommentFetcher.query = query ?? searchCommentFetcher.query\n        searchCommentFetcher.listing = listing ?? searchCommentFetcher.listing\n        searchCommentFetcher.sort = sort ?? searchCommentFetcher.sort\n        try await refresh(clearBeforeRefresh: clearBeforeRefresh)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Community/CommunityFeedLoader.swift",
    "content": "//\n//  CommunityFeedLoader.swift\n//\n//\n//  Created by Sjmarf on 08/09/2024.\n//\n\nimport Foundation\n\n@Observable\nclass CommunityFetcher: Fetcher<Community> {\n    var query: String\n    var listing: ListingType\n    var sort: SearchSortType\n    var hostApi: ApiClient?\n    \n    init(\n        api: ApiClient,\n        query: String,\n        pageSize: Int,\n        listing: ListingType,\n        sort: SearchSortType,\n        hostApi: ApiClient?\n    ) {\n        self.query = query\n        self.listing = listing\n        self.sort = sort\n        self.hostApi = hostApi\n        \n        super.init(api: api, pageSize: pageSize)\n    }\n    \n    override func fetchPage(_ page: Int) async throws -> FetchResponse {\n        let communities = try await api.searchCommunities(\n            query: query,\n            page: page,\n            limit: pageSize,\n            filter: listing,\n            sort: sort,\n            hostApi: hostApi\n        )\n        \n        return .init(\n            items: communities,\n            prevCursor: nil,\n            nextCursor: nil\n        )\n    }\n}\n\n@Observable\npublic class CommunityFeedLoader: StandardFeedLoader<Community> {\n    public var api: ApiClient\n    \n    // force unwrap because this should ALWAYS be a CommunityFetcher\n    var communityFetcher: CommunityFetcher { fetcher as! CommunityFetcher }\n    \n    public init(\n        api: ApiClient,\n        query: String = \"\",\n        pageSize: Int = 20,\n        listing: ListingType = .all,\n        sort: SearchSortType = .top(.allTime),\n        hostApi: ApiClient? = nil\n    ) {\n        self.api = api\n\n        super.init(\n            filter: .init(),\n            fetcher: CommunityFetcher(\n                api: api,\n                query: query,\n                pageSize: pageSize,\n                listing: listing,\n                sort: sort,\n                hostApi: hostApi\n            )\n        )\n    }\n    \n    public func changeApi(to client: ApiClient, context: FilterContext, hostApi: ApiClient?) async {\n        await super.changeApi(to: client, context: context)\n        communityFetcher.hostApi = hostApi\n    }\n    \n    public func refresh(\n        query: String? = nil,\n        listing: ListingType? = nil,\n        sort: SearchSortType? = nil,\n        clearBeforeRefresh: Bool = false\n    ) async throws {\n        communityFetcher.query = query ?? communityFetcher.query\n        communityFetcher.listing = listing ?? communityFetcher.listing\n        communityFetcher.sort = sort ?? communityFetcher.sort\n        try await refresh(clearBeforeRefresh: clearBeforeRefresh)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/DualSourceMixed/CommentChildFeedLoader.swift",
    "content": "//\n//  CommentChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by sjmarf on 2025-10-29.\n//\n\npublic class CommentChildFeedLoader: ChildFeedLoader<PersonContent> {\n    class Fetcher: MlemMiddleware.Fetcher<PersonContent> {\n        let filter: GetContentFilter\n        \n        init(\n            api: ApiClient,\n            pageSize: Int,\n            page: Int = 0,\n            filter: GetContentFilter\n        ) {\n            self.filter = filter\n            super.init(api: api, pageSize: pageSize, page: page)\n        }\n        \n        override func fetchPage(_ page: Int) async throws -> FetchResponse {\n            try await internalFetchCursor(page: page, cursor: nil)\n        }\n\n        override func fetchCursor(_ cursor: String) async throws -> FetchResponse {\n            try await internalFetchCursor(page: nil, cursor: cursor)\n        }\n\n        private func internalFetchCursor(page: Int?, cursor: String?) async throws -> FetchResponse {\n            let response = try await api.getCommentHistory(\n                type: self.filter,\n                page: page,\n                cursor: cursor,\n                limit: pageSize\n            )\n\n            return .init(\n                items: response.comments.map { PersonContent(wrappedValue: .comment($0)) },\n                prevCursor: cursor,\n                nextCursor: response.cursor\n            )\n        }\n    }\n\n    public init(\n        api: ApiClient,\n        pageSize: Int,\n        sortType: FeedLoaderSort.SortType,\n        filter: GetContentFilter\n    ) {\n        super.init(\n            filter: MultiFilter(),\n            fetcher: Fetcher(api: api, pageSize: pageSize, filter: filter),\n            sortType: sortType\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/DualSourceMixed/DualSourceMixedFeedLoader.swift",
    "content": "//\n//  DualSourceMixedFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-10-29.\n//\n\nimport Foundation\n\n// A feed loader that loads both posts and comments, with two child feed loaders that fetch from different sources.\npublic class DualSourceMixedFeedLoader: StandardFeedLoader<PersonContent> {\n    public init(\n        api: ApiClient,\n        pageSize: Int,\n        sources: [ChildFeedLoader<PersonContent>],\n        sortType: FeedLoaderSort.SortType\n    ) {\n        super.init(\n            filter: MultiFilter(),\n            fetcher: MultiFetcher(\n                api: api,\n                pageSize: pageSize,\n                sources: sources,\n                sortType: sortType\n            )\n        )\n        \n        for source in sources {\n            source.setParent(parent: self)\n        }\n    }\n    \n    public static func setup(\n        api: ApiClient,\n        pageSize: Int,\n        sortType: FeedLoaderSort.SortType,\n        filter: GetContentFilter\n    ) -> (\n        postFeedLoader: PostChildFeedLoader,\n        commentFeedLoader: CommentChildFeedLoader,\n        savedFeedLoader: DualSourceMixedFeedLoader\n    ) {\n        let postFeedLoader: PostChildFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            filter: filter\n        )\n        \n        let commentFeedLoader: CommentChildFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            filter: filter\n        )\n        \n        let savedFeedLoader: DualSourceMixedFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sources: [postFeedLoader, commentFeedLoader],\n            sortType: sortType\n        )\n        \n        return (postFeedLoader, commentFeedLoader, savedFeedLoader)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/DualSourceMixed/PostChildFeedLoader.swift",
    "content": "//\n//  PostChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by sjmarf on 2025-10-29.\n//\n\npublic class PostChildFeedLoader: ChildFeedLoader<PersonContent> {\n    class Fetcher: MlemMiddleware.Fetcher<PersonContent> {\n        let filter: GetContentFilter\n        \n        init(\n            api: ApiClient,\n            pageSize: Int,\n            page: Int = 0,\n            filter: GetContentFilter\n        ) {\n            self.filter = filter\n            super.init(api: api, pageSize: pageSize, page: page)\n        }\n        \n        override func fetchPage(_ page: Int) async throws -> FetchResponse {\n            try await internalFetchCursor(page: page, cursor: nil)\n        }\n\n        override func fetchCursor(_ cursor: String) async throws -> FetchResponse {\n            try await internalFetchCursor(page: nil, cursor: cursor)\n        }\n\n        private func internalFetchCursor(page: Int?, cursor: String?) async throws -> FetchResponse {\n            let response = try await api.getPostHistory(\n                type: self.filter,\n                page: page,\n                cursor: cursor,\n                limit: pageSize\n            )\n\n            return .init(\n                items: response.posts.map { PersonContent(wrappedValue: .post($0)) },\n                prevCursor: cursor,\n                nextCursor: response.cursor\n            )\n        }\n    }\n\n    public init(\n        api: ApiClient,\n        pageSize: Int,\n        sortType: FeedLoaderSort.SortType,\n        filter: GetContentFilter\n    ) {\n        super.init(\n            filter: MultiFilter(),\n            fetcher: Fetcher(api: api, pageSize: pageSize, filter: filter),\n            sortType: sortType\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/FilterContext.swift",
    "content": "//\n//  FilterContext.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-12-22.\n//\n\nimport Foundation\n\n/// Information required to perform filtering\npublic struct FilterContext {\n    public let isAdmin: Bool\n    public let moderatedCommunityActorIds: Set<ActorIdentifier>\n    public let filteredKeywords: Set<String>\n    public let filteredPhrases: Set<[String]>\n    public let filteredLiterals: Set<String>\n    \n    public init(\n        isAdmin: Bool,\n        moderatedCommunityActorIds: Set<ActorIdentifier>,\n        filteredKeywords: Set<String>,\n        filteredPhrases: Set<[String]>,\n        filteredLiterals: Set<String>\n    ) {\n        self.isAdmin = isAdmin\n        self.moderatedCommunityActorIds = moderatedCommunityActorIds\n        self.filteredKeywords = filteredKeywords\n        self.filteredPhrases = filteredPhrases\n        self.filteredLiterals = filteredLiterals\n    }\n    \n    static func none() -> FilterContext {\n        .init(isAdmin: true, moderatedCommunityActorIds: [], filteredKeywords: [], filteredPhrases: [], filteredLiterals: [])\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/FilterProviding.swift",
    "content": "//\n//  FilterProviding.swift\n//\n//\n//  Created by Eric Andrews on 2024-05-31.\n//\n\nimport Foundation\n\nclass FilterProviding<FilterTarget> {\n    var context: FilterContext\n    \n    /// How many items this filter has caught\n    var numFiltered: Int = 0\n    var active: Bool = true\n    \n    init(context: FilterContext) {\n        self.context = context\n    }\n    \n    /// Given a list of `FilterTarget`s, returns all members that pass the filter and tracks how many members do not\n    /// - Parameter targets: list of `FilterTarget`s to filter\n    func filter(_ targets: [FilterTarget]) -> [FilterTarget] {\n        let ret = targets.filter(shouldPassFilter)\n        numFiltered += targets.count - ret.count\n        return ret\n    }\n    \n    /// Clears the filter and processes all provided targets\n    /// - Parameter targets: optional list of `FilterTarget`s; if present, these will be filtered and the results returned\n    func reset(with targets: [FilterTarget]?) -> [FilterTarget] {\n        numFiltered = 0\n        if let targets { return filter(targets) }\n        return .init()\n    }\n    \n    /// Returns true if the given post should pass the filter, false otherwise\n    public func shouldPassFilter(_ item: FilterTarget) -> Bool {\n        preconditionFailure(\"This method must be implemented by the inheriting class\")\n    }\n    \n    \n    func updateFilterContext(to context: FilterContext) {\n        self.context = context\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/Filterable.swift",
    "content": "//\n//  Filterable.swift\n//\n//\n//  Created by Eric Andrews on 2024-06-03.\n//\n\nimport Foundation\n\npublic protocol Filterable {\n    associatedtype FilterType\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/Filters/CommentFilter.swift",
    "content": "//\n//  CommentFilter.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-10.\n//\n\nimport Foundation\n\npublic enum CommentFilterType {\n    case todo\n}\n\npublic enum CommunityFilterType {\n    case todo\n}\n\npublic enum PersonFilterType {\n    case todo\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/Filters/DedupeFilter.swift",
    "content": "//\n//  PostDedupeFilter.swift\n//\n//\n//  Created by Eric Andrews on 2024-05-31.\n//\n\nimport Foundation\n\n/// Filter that dedupes ActorIdentifiable items by actorId\nclass DedupeFilter<FilterTarget: ActorIdentifiable>: FilterProviding<FilterTarget> {\n    private var seen: Set<ActorIdentifier> = .init()\n    \n    override func reset(with targets: [FilterTarget]?) -> [FilterTarget] {\n        numFiltered = 0\n        seen = .init()\n        if let targets { return filter(targets) }\n        return .init()\n    }\n    \n    override public func shouldPassFilter(_ item: FilterTarget) -> Bool {\n        seen.insert(item.actorId).inserted\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/Filters/InboxDedupeFilter.swift",
    "content": "//\n//  InboxDedupeFilter.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-02-01.\n//\n\n/// Filter that dedupes InboxIdentifiable items by inboxId\nclass InboxDedupeFilter<FilterTarget: InboxIdentifiable>: FilterProviding<FilterTarget> {\n    private var seen: Set<Int> = .init()\n    \n    override func reset(with targets: [FilterTarget]?) -> [FilterTarget] {\n        numFiltered = 0\n        seen = .init()\n        if let targets { return filter(targets) }\n        return .init()\n    }\n    \n    override public func shouldPassFilter(_ item: FilterTarget) -> Bool {\n        seen.insert(item.inboxId).inserted\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/Filters/Post/PostKeywordFilter.swift",
    "content": "//\n//  PostKeywordFilter.swift\n//\n//\n//  Created by Eric Andrews on 2024-06-02.\n//\n\nimport Foundation\n\nclass PostKeywordFilter: FilterProviding<Post> {\n    override public func shouldPassFilter(_ post: Post) -> Bool {\n        // community should always exist for posts going through the feed loader\n        guard let community = post.community.value_ else {\n            assertionFailure(\"No community found in filter-eligible post\")\n            return true\n        }\n        // bypass filter for moderated/administrated posts\n        if context.isAdmin || context.moderatedCommunityActorIds.contains(community.actorId) { return true }\n        \n        return !post.title.failsKeywordFilter(keywords: context.filteredKeywords, phrases: context.filteredPhrases)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/Filters/Post/PostLiteralFilter.swift",
    "content": "//\n//  PostLiteralFilter.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-11-18.\n//\n\nimport Foundation\n\nclass PostLiteralFilter: FilterProviding<Post> {\n    override public func shouldPassFilter(_ post: Post) -> Bool {\n        // community should always exist for posts going through the feed loader\n        guard let community = post.community.value_ else {\n            assertionFailure(\"No community found in filter-eligible post\")\n            return true\n        }\n        // bypass filter for moderated/administrated posts\n        if context.isAdmin || context.moderatedCommunityActorIds.contains(community.actorId) { return true }\n        \n        return !post.title.failsLiteralFilter(literals: context.filteredLiterals)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/Filters/ReadFilter.swift",
    "content": "//\n//  ReadFilter.swift\n//\n//\n//  Created by Eric Andrews on 2024-05-31.\n//\n\nimport Foundation\n\nclass ReadFilter<FilterTarget: ReadableProviding>: FilterProviding<FilterTarget> {\n    override public func shouldPassFilter(_ item: FilterTarget) -> Bool {\n        return !item.read\n    }\n}\n\nclass UnifiedReadFilter<FilterTarget: UnifiedReadableProviding>: FilterProviding<FilterTarget> {\n    override public func shouldPassFilter(_ item: FilterTarget) -> Bool {\n        return !(item.read.value_ ?? false)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/Filters/UserContentFilter.swift",
    "content": "//\n//  PersonContentFilterType.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-18.\n//\n\nimport Foundation\n\npublic enum PersonContentFilterType {\n    case todo\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/InboxItemFilter.swift",
    "content": "//\n//  InboxItemFilter.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-12-04.\n//\n\npublic enum InboxItemFilterType {\n    case read, dedupe\n}\n\nclass InboxItemFilter: MultiFilter<InboxNotification> {\n    private var readFilter: ReadFilter<InboxNotification>\n    private var dedupeFilter: InboxDedupeFilter<InboxNotification> = .init(context: .none())\n    \n    init(showRead: Bool) {\n        self.readFilter = .init(context: .none())\n        if showRead {\n            readFilter.active = false\n        }\n    }\n\n    override func allFilters() -> [FilterProviding<InboxNotification>] {\n        [\n            readFilter,\n            dedupeFilter\n        ]\n    }\n    \n    override func getFilter(_ toGet: InboxItemFilterType) -> FilterProviding<InboxNotification> {\n        switch toGet {\n        case .read: readFilter\n        case .dedupe: dedupeFilter\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/ModMailItemFilter.swift",
    "content": "//\n//  ModMailItemFilter.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-01-31.\n//\n\npublic enum ModMailItemFilterType {\n    case read, dedupe\n}\n\nclass ModMailItemFilter: MultiFilter<ModMailItem> {\n    private var readFilter: ReadFilter<ModMailItem>\n    private var dedupeFilter: InboxDedupeFilter<ModMailItem> = .init(context: .none())\n    \n    init(showRead: Bool) {\n        self.readFilter = .init(context: .none())\n        if showRead {\n            readFilter.active = false\n        }\n    }\n\n    override func allFilters() -> [FilterProviding<ModMailItem>] {\n        [\n            readFilter,\n            dedupeFilter\n        ]\n    }\n    \n    override func getFilter(_ toGet: ModMailItemFilterType) -> FilterProviding<ModMailItem> {\n        switch toGet {\n        case .read: readFilter\n        case .dedupe: dedupeFilter\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/ModlogItemFilter.swift",
    "content": "//\n//  ModlogItemFilter.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-28.\n//\n\nimport Foundation\n\npublic enum ModlogEntryFilterType {}\n\nclass ModlogEntryFilter: MultiFilter<ModlogEntry> {\n    override func allFilters() -> [FilterProviding<ModlogEntry>] {\n        []\n    }\n    \n    override func getFilter(_ toGet: ModlogEntryFilterType) -> FilterProviding<ModlogEntry> {}\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/MultiFilter.swift",
    "content": "//\n//  MultiFilter.swift\n//\n//\n//  Created by Eric Andrews on 2024-06-03.\n//\n\nimport Foundation\n\nclass MultiFilter<FilterTarget: Filterable> {\n    var numFiltered: Int { allFilters().reduce(0) { $0 + $1.numFiltered } }\n    \n    /// Lists all filters in this MultiFilter. Used internally to iterate over filters and perform filtering logic. This function bridges the gap between the generic behavior, which wants a list of `[any FilterProviding<FilterTarget>]` to use in filtering, and the instantiating class, which is far more ergonomic if filters can be declared as simple member variables.\n    /// - Returns: list of all filters in this MultiFilter\n    func allFilters() -> [FilterProviding<FilterTarget>] { [] }\n    \n    /// Gets a particular optional filter. Used internally to back the `activate`, `deactivate`, and `filteredCount` methods; as with `allFilters`, used to bridge generic and concrete behavior.\n    /// - Parameter toGet: `OptionalFilters` describing the filter to get\n    /// - Returns: filter corresponding to `toGet`\n    func getFilter(_ toGet: FilterTarget.FilterType) -> FilterProviding<FilterTarget> {\n        preconditionFailure(\"This method must be implemented by the instantiating class\")\n    }\n    \n    func filter(_ targets: [FilterTarget]) -> [FilterTarget] {\n        var ret: [FilterTarget] = targets\n        for filter in allFilters() where filter.active {\n            ret = filter.filter(ret)\n        }\n        return ret\n    }\n    \n    /// Resets this filter and all its children\n    /// - Parameter targets optional; if present, will immediately re-filter all targets\n    /// - Returns result of filtering targets, if present, otherwise an empty array\n    @discardableResult\n    func reset(with targets: [FilterTarget] = .init()) -> [FilterTarget] {\n        var ret = targets\n        for filter in allFilters() {\n            if filter.active {\n                ret = filter.reset(with: ret)\n            } else {\n                _ = filter.reset(with: nil)\n            }\n        }\n        return ret\n    }\n    \n    /// Activates the given filter\n    /// - Parameter filter: filter to activate\n    /// - Returns: true if the filter was successfully activated, false if it was already active\n    func activate(_ toActivate: FilterTarget.FilterType) -> Bool {\n        let filter = getFilter(toActivate)\n        let ret = !filter.active\n        filter.active = true\n        return ret\n    }\n    \n    /// Deactivates the given filter\n    /// - Parameter filter: filter to deactivate\n    /// - Returns: true if the filter was successfully deactivated, false if it was already inactive\n    func deactivate(_ toDeactivate: FilterTarget.FilterType) -> Bool {\n        let filter = getFilter(toDeactivate)\n        let ret = filter.active\n        filter.active = false\n        return ret\n    }\n    \n    func numFiltered(for filter: FilterTarget.FilterType) -> Int {\n        getFilter(filter).numFiltered\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Filtering/PostFilter.swift",
    "content": "//\n//  PostFilter.swift\n//\n//\n//  Created by Eric Andrews on 2024-05-31.\n//\n\nimport Foundation\n\npublic enum PostFilterType {\n    case read, dedupe, keyword, literal\n}\n\nclass PostFilter: MultiFilter<Post> {\n    private var readFilter: UnifiedReadFilter<Post>\n    private var dedupeFilter: DedupeFilter<Post> = .init(context: .none())\n    private var keywordFilter: PostKeywordFilter\n    private var literalFilter: PostLiteralFilter\n    \n    init(showRead: Bool, context: FilterContext) {\n        self.keywordFilter = .init(context: context)\n        self.literalFilter = .init(context: context)\n        self.readFilter = .init(context: .none())\n        if showRead {\n            readFilter.active = false\n        }\n    }\n\n    override func allFilters() -> [FilterProviding<Post>] {\n        [\n            readFilter,\n            dedupeFilter,\n            keywordFilter,\n            literalFilter\n        ]\n    }\n    \n    override func getFilter(_ toGet: PostFilterType) -> FilterProviding<Post> {\n        switch toGet {\n        case .read: readFilter\n        case .dedupe: dedupeFilter\n        case .keyword: keywordFilter\n        case .literal: literalFilter\n        }\n    }\n    \n    // MARK: Custom Behavior\n    \n    func updateContext(to context: FilterContext) {\n        keywordFilter.updateFilterContext(to: context)\n        literalFilter.updateFilterContext(to: context)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Generics/ChildFeedLoader.swift",
    "content": "//\n//  ChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-24.\n//\n\n/// Helper class bundling a parent feed loader and a position in a ChildFeedLoader's item list\nclass FeedLoaderStream {\n    weak var parent: (any FeedLoading)?\n    var cursor: Int\n    \n    init(parent: (any FeedLoading)? = nil) {\n        self.parent = parent\n        self.cursor = 0\n    }\n}\n\npublic class ChildFeedLoader<Item: FeedLoadable>: StandardFeedLoader<Item> {\n    var stream: FeedLoaderStream?\n    var sortType: FeedLoaderSort.SortType\n    \n    init(filter: MultiFilter<Item>, fetcher: Fetcher<Item>, sortType: FeedLoaderSort.SortType) {\n        self.sortType = sortType\n        \n        super.init(filter: filter, fetcher: fetcher)\n    }\n    \n    public func setParent(parent: any FeedLoading<Item>) {\n        stream = .init(parent: parent)\n    }\n    \n    public func nextItemSortVal(sortType: FeedLoaderSort.SortType) async throws -> FeedLoaderSort? {\n        assert(sortType == self.sortType, \"Conflicting types for sortType! This will lead to unexpected sorting behavior.\")\n        \n        guard let stream, stream.parent != nil else {\n            log.info(\"[\\(Self.self)] could not find stream or parent\")\n            return nil\n        }\n        \n        if stream.cursor < items.count {\n            return items[safeIndex: stream.cursor]?.sortVal(sortType: sortType)\n        } else {\n            if loadingState == .done {\n                log.debug(\"[\\(Self.self)] done loading\")\n                return nil\n            }\n            \n            log.debug(\"[\\(Self.self)] out of items (\\(self.items.count)), loading more\")\n            try await loadMoreItems()\n            \n            if stream.cursor >= items.count {\n                // NOTE: this assertion can sometimes be tripped by spamming the filter button\n                assert(loadingState == .done, \"[\\(Item.self) ChildFeedLoader] Invalid loading state \\(self.loadingState)\")\n                return nil\n            }\n            \n            log.debug(\"[\\(Self.self)] fetched more items (\\(self.items.count))\")\n            return items[stream.cursor].sortVal(sortType: sortType)\n        }\n    }\n    \n    public func consumeNextItem() -> Item? {\n        guard let stream, stream.parent != nil else {\n            assertionFailure(\"[\\(Item.self)] could not find stream or parent\")\n            return nil\n        }\n        \n        stream.cursor += 1\n        return items[safeIndex: stream.cursor - 1]\n    }\n    \n    public func clear(clearParent: Bool) async {\n        if clearParent {\n            await stream?.parent?.clear()\n        }\n        \n        stream?.cursor = 0\n        await super.clear()\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Generics/FeedLoaderItem.swift",
    "content": "//\n//  FeedLoadable.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-10-15.\n//\n\nimport Foundation\n\npublic protocol FeedLoadable: Filterable, Equatable {\n    associatedtype FilterType\n    var api: ApiClient { get }\n    \n    func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Generics/FeedLoaderSort.swift",
    "content": "//\n//  FeedLoaderSort.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-10-15.\n//\nimport Foundation\n\npublic enum FeedLoaderSort: Comparable {\n    case new(Date)\n    \n    public enum SortType {\n        case new\n    }\n}\n\npublic extension FeedLoaderSort {\n    var sortType: FeedLoaderSort.SortType {\n        switch self {\n        case .new: .new\n        }\n    }\n    \n    var apiType: LemmySortType {\n        switch self {\n        case .new: .new\n        }\n    }\n    \n    func typeEquals(lhs: FeedLoaderSort, rhs: FeedLoaderSort) -> Bool {\n        lhs.sortType == rhs.sortType\n    }\n    \n    /// Compares two FeedLoaderSorts. Returns true if rhs should be sorted after lhs. Assumes that higher items should be sorted first; thus for some sorts (e.g., \"old\"), the result will be \"flipped,\" since _lower_ dates should be sorted ahead of higher ones.\n    static func < (lhs: FeedLoaderSort, rhs: FeedLoaderSort) -> Bool {\n        guard lhs.sortType == rhs.sortType else {\n            assertionFailure(\"Compare called on TrackerSorts with different types\")\n            return true\n        }\n        \n        switch lhs {\n        case let .new(lhsDate):\n            switch rhs {\n            case let .new(rhsDate):\n                return lhsDate < rhsDate\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Generics/FeedLoading.swift",
    "content": "//\n//  FeedLoading.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-05.\n//\n\nimport Foundation\n\npublic protocol FeedLoading<Item>: AnyObject {\n    associatedtype Item: FeedLoadable\n    \n    var items: [Item] { get }\n    var loadingState: FeedLoadingState { get }\n    \n    func loadMoreItems() async throws\n    func loadIfThreshold(_ item: Item) throws\n    func refresh(clearBeforeRefresh: Bool) async throws\n    func clear() async\n    func changeApi(to newApi: ApiClient, context: FilterContext) async\n    \n    /// Adds the given item to the beginning of the items array, regardless of whether it should be filtered\n    /// - Warning: when using this method with multi-feed loaders, you must call this on both the parent loader and the relevant child loader!\n    func prependItem(_ newItem: Item)\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Generics/FeedLoadingState.swift",
    "content": "//\n//  FeedLoadingState.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-10-12.\n//\nimport Foundation\n\n/// Enum of possible loading states that a loader can be in.\n/// - idle: not currently loading, but more items available to load\n/// - loading: currently loading more items\n/// - done: no more items available to load\npublic enum LoadingState: Hashable {\n    case idle, loading, done\n}\n\npublic enum FeedLoadingState: Hashable {\n    case initial, idle, loading, done\n\n    public init(from loadingState: LoadingState) {\n        self = switch loadingState {\n        case .idle: .idle\n        case .loading: .loading\n        case .done: .done\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Generics/Fetcher.swift",
    "content": "//\n//  Fetcher.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-17.\n//\n\nimport Observation\nimport os\n\nenum LoadingResponse<Item: FeedLoadable> {\n    /// Indicates a successful load with more items available to fetch\n    case success([Item])\n    \n    /// Indicates a successful load with no more items available to fetch\n    case done([Item])\n    \n    /// Indicates the load was ignored due to an existing ongoing load\n    case ignored\n    \n    /// Indicates the load was cancelled\n    case cancelled\n    \n    var description: String {\n        switch self {\n        case let .success(items): \"success (\\(items.count))\"\n        case let .done(items): \"done (\\(items.count))\"\n        case .ignored: \"ignored\"\n        case .cancelled: \"cancelled\"\n        }\n    }\n}\n\n@Observable\npublic class Fetcher<Item: FeedLoadable> {\n    let log: Logger = .mlemLogger()\n    \n    var api: ApiClient\n    var pageSize: Int\n    var page: Int\n    private var cursor: String?\n    \n    init(api: ApiClient, pageSize: Int, page: Int = 0) {\n        self.api = api\n        self.pageSize = pageSize\n        self.page = page\n    }\n    \n    /// Helper struct bundling the response from a fetchPage or fetchCursor call\n    struct FetchResponse {\n        /// Items returned\n        let items: [Item]\n        \n        /// Cursor used to fetch this response, if applicable\n        let prevCursor: String?\n        \n        /// New cursor, if applicable\n        let nextCursor: String?\n    }\n    \n    /// Fetches the next page of items\n    func fetch() async throws -> LoadingResponse<Item> {\n        do {\n            if let cursor, page > 0 {\n                log.debug(\"[\\(Item.self) Fetcher] loading cursor \\(cursor)\")\n                let response = try await fetchCursor(cursor)\n                \n                // if same cursor returned, loading is finished. On Lemmy 1.0, if no cursor is returned, loading is finished.\n                if response.nextCursor == self.cursor || (self.cursor != nil && response.nextCursor == nil) {\n                    return .done(response.items)\n                }\n                \n                self.cursor = response.nextCursor\n                return .success(response.items)\n            } else {\n                page += 1\n                log.debug(\"[\\(Item.self) Fetcher] loading page \\(self.page)\")\n                let response = try await fetchPage(page)\n                \n                // if nothing returned, loading is finished\n                if response.items.count < pageSize {\n                    log.debug(\"[\\(Item.self) Fetcher] received undersized page (\\(response.items.count)/\\(self.pageSize))\")\n                    return .done(response.items)\n                }\n                cursor = response.nextCursor\n                return .success(response.items)\n            }\n        } catch is CancellationError {\n            return .cancelled\n        }\n    }\n    \n    /// Fetches the given page of items.\n    /// - Parameters:\n    ///   - page: page number to fetch\n    /// - Returns: tuple of the requested page of items, the cursor returned by the API call (if present), and the number of items that were filtered out.\n    func fetchPage(_ page: Int) async throws -> FetchResponse {\n        preconditionFailure(\"This method must be implemented by the inheriting class\")\n    }\n    \n    /// Fetches items from the given cursor.\n    /// - Parameters:\n    ///   - cursor: cursor to fetch\n    /// - Returns: tuple of the requested page of items, the cursor returned by the API call (if present), and the number of items that were filtered out.\n    func fetchCursor(_ cursor: String) async throws -> FetchResponse {\n        preconditionFailure(\"This method must be implemented by the inheriting class\")\n    }\n    \n    /// Resets the fetcher's page and cursor tracking. This method should only be overridden to handle abnormal pagination behavior (e.g., SingleSourceMixedFetcher); it should NOT change loading parameters such as query or sort.\n    func reset() async {\n        page = 0\n        cursor = nil\n    }\n    \n    func changeApi(to newApi: ApiClient, context: FilterContext) async {\n        api = newApi\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Generics/LoadingActor.swift",
    "content": "//\n//  LoadingActor.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-10-27.\n//\n\nimport Foundation\nimport os\n\nenum LoadingError: Error {\n    case noTask\n}\n\nactor LoadingActor<Item: FeedLoadable> {\n    internal let log: Logger = .mlemLogger()\n    \n    private var done: Bool = false\n    private var loadingTask: Task<Void, Error>?\n    var filter: MultiFilter<Item>\n  \n    private var fetcher: Fetcher<Item>\n    \n    public init(fetcher: Fetcher<Item>, filter: MultiFilter<Item>) {\n        self.fetcher = fetcher\n        self.filter = filter\n    }\n    \n    /// Cancels any ongoing loading and resets the page/cursor to 0\n    func reset() async {\n        loadingTask?.cancel()\n        loadingTask = nil\n        filter.reset()\n        await fetcher.reset()\n        done = false\n    }\n    \n    /// Loads the next page of items.\n    /// - Returns: on success, .success with FetchResponse containing loaded items; if another load is underway, .ignored; if the load is cancelled, .cancelled\n    func load(_ callback: @escaping (LoadingResponse<Item>) async -> Void) async throws {\n        guard !done else {\n            log.debug(\"[\\(Self.self)] ignoring request, finished loading\")\n            return\n        }\n        \n        // if already loading something, ignore the request\n        if let loadingTask {\n            log.debug(\"[\\(Self.self)] ignoring request, load underway\")\n            // return .ignored\n            _ = try await loadingTask.result.get()\n            log.debug(\"[\\(Self.self)] preexisting load finished, returning\")\n            return\n        }\n        \n        // upon completion of load, remove loading task\n        defer { loadingTask = nil }\n        \n        loadingTask = Task<Void, Error> {\n            let response = try await fetchMoreItems()\n            await callback(response)\n        }\n        \n        guard let loadingTask else {\n            assertionFailure(\"loadingTask is nil!\")\n            throw LoadingError.noTask\n        }\n        \n        _ = try await loadingTask.result.get()\n        log.info(\"[\\(Self.self)] finished loading\")\n    }\n    \n    @discardableResult\n    func filterItem(_ target: Item) -> Item? {\n        let filtered = filter.filter([target])\n        return filtered.first\n    }\n    \n    func activateFilter(_ target: Item.FilterType, callback: () async throws -> Void) async throws {\n        loadingTask?.cancel()\n        loadingTask = nil\n        if filter.activate(target) {\n            try await callback()\n        }\n    }\n    \n    func deactivateFilter(_ target: Item.FilterType, callback: () async throws -> Void) async throws {\n        loadingTask?.cancel()\n        loadingTask = nil\n        if filter.deactivate(target) {\n            try await callback()\n        }\n    }\n    \n    // MARK: Helpers\n    \n    private func fetchMoreItems() async throws -> LoadingResponse<Item> {\n        var newItems: [Item] = .init()\n        fetchLoop: repeat {\n            let response = try await fetcher.fetch()\n            \n            switch response {\n            case let .success(items):\n                log.debug(\"[\\(Self.self)] received success (\\(items.count))\")\n                newItems.append(contentsOf: filter.filter(items))\n            case let .done(items):\n                log.debug(\"[\\(Self.self)] received finished (\\(items.count))\")\n                newItems.append(contentsOf: filter.filter(items))\n                return .done(newItems)\n            case .cancelled, .ignored:\n                log.info(\"[\\(Self.self)] load did not complete (\\(response.description))\")\n                break fetchLoop\n            }\n        } while newItems.count < MiddlewareConstants.infiniteLoadThresholdOffset\n\n        return .success(newItems)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Generics/MultiFetcher.swift",
    "content": "//\n//  MultiFetcher.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-24.\n//\n\nimport Observation\n\n@Observable\nclass MultiFetcher<Item: FeedLoadable>: Fetcher<Item> {\n    var sources: [ChildFeedLoader<Item>]\n    var sortType: FeedLoaderSort.SortType\n    \n    init(api: ApiClient, pageSize: Int, sources: [ChildFeedLoader<Item>], sortType: FeedLoaderSort.SortType) {\n        self.sources = sources\n        self.sortType = sortType\n        \n        super.init(api: api, pageSize: pageSize)\n    }\n    \n    override func fetch() async throws -> LoadingResponse<Item> {\n        var newItems: [Item] = .init()\n        \n        while newItems.count < pageSize {\n            if let nextItem = try await computeNextItem() {\n                newItems.append(nextItem)\n            } else {\n                log.debug(\"[\\(Self.self)] no next item found\")\n                return .done(newItems)\n            }\n        }\n        \n        return .success(newItems)\n    }\n    \n    override func reset() async {\n        for source in sources {\n            await source.clear(clearParent: false)\n            log.debug(\"[\\(Self.self)] source cleared (\\(String(describing: source.loadingState)))\")\n        }\n        \n        await super.reset()\n    }\n    \n    /// Computes and returns the highest sorted item from the tops of all sources\n    private func computeNextItem() async throws -> Item? {\n        var sortVal: FeedLoaderSort?\n        var sourceToConsume: ChildFeedLoader<Item>?\n        \n        // find the highest-sorted item from the tops of all sources\n        for source in sources {\n            (sortVal, sourceToConsume) = try await compareNextItem(lhsVal: sortVal, lhsSource: sourceToConsume, rhsSource: source)\n        }\n        \n        return sourceToConsume?.consumeNextItem()\n    }\n    \n    private func compareNextItem(\n        lhsVal: FeedLoaderSort?,\n        lhsSource: ChildFeedLoader<Item>?,\n        rhsSource: ChildFeedLoader<Item>\n    ) async throws -> (FeedLoaderSort?, ChildFeedLoader<Item>?) {\n        // if no next item on rhs, return lhs (even if null)\n        guard let rhsVal = try await rhsSource.nextItemSortVal(sortType: sortType) else {\n            return (lhsVal, lhsSource)\n        }\n        \n        // if no lhsVal, rhs next by default\n        guard let lhsVal else {\n            return (rhsVal, rhsSource)\n        }\n        \n        return lhsVal > rhsVal ? (lhsVal, lhsSource) : (rhsVal, rhsSource)\n    }\n    \n    override func changeApi(to newApi: ApiClient, context: FilterContext) async {\n        for source in sources {\n            await source.changeApi(to: newApi, context: context)\n        }\n        \n        await super.changeApi(to: newApi, context: context)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Generics/PrefetchingFeedLoader.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-25.\n//\n\nimport Foundation\nimport Nuke\nimport Observation\n\n@Observable\npublic class PrefetchingFeedLoader<Item: ImagePrefetchProviding & FeedLoadable>: StandardFeedLoader<Item> {\n    public private(set) var prefetchingConfiguration: PrefetchingConfiguration\n\n    init(\n        filter: MultiFilter<Item>,\n        prefetchingConfiguration: PrefetchingConfiguration,\n        fetcher: Fetcher<Item>\n    ) {\n        self.prefetchingConfiguration = prefetchingConfiguration\n        \n        super.init(\n            filter: filter,\n            fetcher: fetcher\n        )\n    }\n  \n    override func processNewItems(_ items: [Item]) {\n        prefetchImages(items)\n    }\n\n    private func prefetchImages(_ items: [Item]) {\n        Task {\n            await prefetchingConfiguration.prefetcher.startPrefetching(with: items.concurrentFlatMap { item -> [ImageRequest] in\n                await item.imageRequests(configuration: self.prefetchingConfiguration)\n            })\n        }\n    }\n    \n    public func setPrefetchingConfiguration(_ config: PrefetchingConfiguration) {\n        prefetchingConfiguration = config\n        prefetchImages(items)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Generics/StandardFeedLoader.swift",
    "content": "//\n//  StandardFeedLoader.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-10-15.\n//\n\nimport Foundation\nimport Observation\nimport os\nimport Semaphore\n\n@Observable\npublic class StandardFeedLoader<Item: FeedLoadable>: FeedLoading {\n    let log: Logger = .mlemLogger()\n    \n    public internal(set) var items: [Item] = .init()\n    public internal(set) var loadingState: FeedLoadingState = .initial\n    private(set) var thresholds: Thresholds<Item> = .init()\n    \n    let fetcher: Fetcher<Item>\n    var loadingActor: LoadingActor<Item>\n\n    init(filter: MultiFilter<Item>, fetcher: Fetcher<Item>) {\n        self.fetcher = fetcher\n        self.loadingActor = .init(fetcher: fetcher, filter: filter)\n    }\n\n    // MARK: - State Modification Methods\n    \n    /// Updates the loading state\n    @MainActor\n    func setLoading(_ newState: FeedLoadingState) {\n        loadingState = newState\n        log.debug(\"[\\(Self.self)] set loading state to \\(String(describing: newState))\")\n    }\n    \n    /// Sets the items to a new array\n    @MainActor\n    func setItems(_ newItems: [Item]) {\n        processNewItems(newItems)\n        items = newItems\n        thresholds.update(with: newItems)\n    }\n    \n    /// Adds the given items to the items array\n    /// - Parameter toAdd: items to add\n    @MainActor\n    func addItems(_ newItems: [Item]) async {\n        processNewItems(newItems)\n        items.append(contentsOf: newItems)\n        thresholds.update(with: newItems)\n    }\n    \n    @MainActor\n    public func prependItem(_ newItem: Item) {\n        items.prepend(newItem)\n        \n        // filter the item on a background thread for deduping\n        Task {\n            await loadingActor.filterItem(newItem)\n        }\n    }\n    \n    // MARK: - External methods\n\n    /// If the given item is the loading threshold item, loads more content\n    /// This should be called as an .onAppear of every item in a feed that should support infinite scrolling\n    public func loadIfThreshold(_ item: Item) throws {\n        if loadingState == .idle, thresholds.isThreshold(item) {\n            // this is a synchronous function that wraps the loading as a task so that the task is attached to the loader itself, not the view that calls it, and is therefore safe from being cancelled by view redraws\n            Task(priority: .userInitiated) {\n                try await loadMoreItems()\n            }\n        }\n    }\n    \n    /// Loads the next page of items. Returns when more items have been added to the items array or loading is complete, even\n    /// if called while another load is underway\n    public func loadMoreItems() async throws {\n        try await loadMoreItems(overwriteExistingItems: false)\n    }\n    \n    /// Internal loadMoreItems() that allows overwriting existing items, used to back refresh\n    func loadMoreItems(overwriteExistingItems: Bool) async throws {\n        await setLoading(.loading)\n  \n        try await loadingActor.load { response in\n            var newItems: [Item]?\n            var newState: FeedLoadingState\n            \n            switch response {\n            case let .success(items):\n                newItems = items\n                newState = items.count > 0 ? .idle : .done\n            case let .done(items):\n                newItems = items\n                newState = .done\n            case .ignored, .cancelled:\n                self.log.info(\"[\\(Self.self)] load did not complete (\\(response.description))\")\n                newState = .idle\n            }\n            \n            if let newItems {\n                if overwriteExistingItems {\n                    await self.setItems(newItems)\n                } else {\n                    await self.addItems(newItems)\n                }\n            }\n            \n            await self.setLoading(newState)\n            self.log.info(\"[\\(Self.self)] loadMoreItems complete\")\n        }\n    }\n    \n    public func refresh(clearBeforeRefresh: Bool) async throws {\n        await setLoading(.loading)\n        \n        if clearBeforeRefresh {\n            await setItems(.init())\n        }\n        \n        await loadingActor.reset()\n    \n        try await loadMoreItems(overwriteExistingItems: true)\n    }\n\n    public func clear() async {\n        await loadingActor.reset()\n        await setItems(.init())\n        await setLoading(.idle)\n    }\n    \n    /// Helper function to perform custom post-fetch processing (e.g., prefetching). Override to implement desired behavior.\n    func processNewItems(_ items: [Item]) {}\n    \n    /// Adds a filter to the tracker, removing all current items that do not pass the filter and filtering out all future items that do not pass the filter.\n    /// Use in situations where filtering is handled client-side (e.g., keywords)\n    /// - Parameter newFilter: Item.FilterType describing the filter to apply\n    public func activateFilter(_ target: Item.FilterType) async throws {\n        try await loadingActor.activateFilter(target) {\n            await setItems(loadingActor.filter.reset(with: items))\n            \n            if items.isEmpty {\n                try await refresh(clearBeforeRefresh: false)\n            } else if thresholds.fallback == nil {\n                // if too few items are present after filtering to trigger threshold loading, initiate new load\n                try await loadMoreItems()\n            }\n        }\n    }\n    \n    public func deactivateFilter(_ target: Item.FilterType) async throws {\n        try await loadingActor.deactivateFilter(target) {\n            try await refresh(clearBeforeRefresh: true)\n        }\n    }\n    \n    public func getFilteredCount(for toCount: Item.FilterType) async -> Int {\n        await loadingActor.filter.numFiltered(for: toCount)\n    }\n    \n    public func changeApi(to newApi: ApiClient, context: FilterContext) async {\n        await fetcher.changeApi(to: newApi, context: context)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Helpers/Thresholds.swift",
    "content": "//\n//  Thresholds.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-22.\n//\n\nimport Foundation\n\nstruct Thresholds<Item: FeedLoadable> {\n    var standard: Item?\n    var fallback: Item?\n    \n    func isThreshold(_ item: Item) -> Bool {\n        item == standard || item == fallback\n    }\n    \n    mutating func update(with newItems: [Item]) {\n        if newItems.count < MiddlewareConstants.infiniteLoadThresholdOffset {\n            standard = nil\n            fallback = nil\n        } else {\n            standard = newItems[newItems.count - MiddlewareConstants.infiniteLoadThresholdOffset]\n            fallback = newItems.last\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/InboxFeedLoader/InboxChildFeedLoader.swift",
    "content": "//\n//  InboxChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-12-04.\n//\n\npublic class InboxChildFeedLoader: ChildFeedLoader<InboxNotification> {\n    var inboxFetcher: InboxFetcher { fetcher as! InboxFetcher }\n    \n    public init(api: ApiClient, sortType: FeedLoaderSort.SortType, fetcher: InboxFetcher, showRead: Bool) {\n        super.init(filter: InboxItemFilter(showRead: showRead), fetcher: fetcher, sortType: sortType)\n    }\n    \n    func hideRead() async throws {\n        try await loadingActor.activateFilter(.read) {\n            await setItems(loadingActor.filter.reset(with: items))\n            inboxFetcher.hideRead(unreadCount: items.count)\n            \n            if items.isEmpty {\n                try await refresh(clearBeforeRefresh: false)\n            }\n        }\n    }\n    \n    func showRead() async throws {\n        try await loadingActor.deactivateFilter(.read) {\n            inboxFetcher.showRead()\n            try await refresh(clearBeforeRefresh: true)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/InboxFeedLoader/InboxFeedLoader.swift",
    "content": "//\n//  InboxFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-27.\n//\n\nimport Foundation\n\npublic class InboxFeedLoader: StandardFeedLoader<InboxNotification> {\n    var inboxFetcher: MultiFetcher<InboxNotification> { fetcher as! MultiFetcher }\n    \n    public init(api: ApiClient, pageSize: Int, sources: [ChildFeedLoader<InboxNotification>], sortType: FeedLoaderSort.SortType, showRead: Bool) {\n        super.init(filter: InboxItemFilter(showRead: showRead), fetcher: MultiFetcher(api: api, pageSize: pageSize, sources: sources, sortType: sortType))\n        \n        for source in sources {\n            source.setParent(parent: self)\n        }\n    }\n    \n    public static func setup(\n        api: ApiClient,\n        pageSize: Int,\n        sortType: FeedLoaderSort.SortType,\n        showRead: Bool\n    ) -> (\n        replyFeedLoader: ReplyChildFeedLoader,\n        mentionFeedLoader: MentionChildFeedLoader,\n        messageFeedLoader: MessageChildFeedLoader,\n        inboxFeedLoader: InboxFeedLoader\n    ) {\n        let replyFeedLoader: ReplyChildFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            showRead: showRead\n        )\n        let mentionFeedLoader: MentionChildFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            showRead: showRead\n        )\n        let messageFeedLoader: MessageChildFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            showRead: showRead\n        )\n        \n        let inboxFeedLoader: InboxFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sources: [replyFeedLoader, mentionFeedLoader, messageFeedLoader],\n            sortType: sortType,\n            showRead: showRead\n        )\n        \n        return (\n            replyFeedLoader,\n            mentionFeedLoader,\n            messageFeedLoader,\n            inboxFeedLoader\n        )\n    }\n    \n    public func hideRead() async throws {\n        await withThrowingTaskGroup(of: Void.self) { group in\n            for source in inboxFetcher.sources {\n                group.addTask {\n                    guard let childSource = source as? InboxChildFeedLoader else {\n                        assertionFailure(\"Child is not InboxChildFeedLoader\")\n                        return\n                    }\n                    try await childSource.hideRead()\n                }\n            }\n        }\n        \n        try await activateFilter(.read)\n    }\n    \n    public func showRead() async throws {\n        await withThrowingTaskGroup(of: Void.self) { group in\n            for source in inboxFetcher.sources {\n                group.addTask {\n                    guard let childSource = source as? InboxChildFeedLoader else {\n                        assertionFailure(\"Child is not InboxChildFeedLoader\")\n                        return\n                    }\n                    try await childSource.showRead()\n                }\n            }\n        }\n\n        try await deactivateFilter(.read)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/InboxFeedLoader/InboxFetcher.swift",
    "content": "//\n//  InboxFetcher.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-24.\n//\n\nimport Foundation\n\n@Observable\npublic class InboxFetcher: Fetcher<InboxNotification> {\n    var unreadOnly: Bool\n    \n    init(api: ApiClient, pageSize: Int, unreadOnly: Bool) {\n        self.unreadOnly = unreadOnly\n        super.init(api: api, pageSize: pageSize)\n    }\n    \n    /// Updates fetching behavior to hide read items. Assumes items will NOT be cleared from the associated FeedLoader and that deduping will be handled by that FeedLoader.\n    /// - Parameter unreadCount: number of unread items still present after client-side filtering\n    func hideRead(unreadCount: Int) {\n        guard !unreadOnly else {\n            assertionFailure(\"Cannot hide read (unreadOnly already true)\")\n            return\n        }\n        \n        unreadOnly = true\n        page = Int(floor(Double(unreadCount / pageSize)))\n    }\n    \n    /// Updates fetching behavior to show read posts. Assumes associated FeedLoader will immediately perform a refresh.\n    func showRead() {\n        guard unreadOnly else {\n            assertionFailure(\"Cannot show read (unreadOnly already false)\")\n            return\n        }\n        \n        unreadOnly = false\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/InboxFeedLoader/MentionChildFeedLoader.swift",
    "content": "//\n//  MentionChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-12-02.\n//\n\npublic class MentionChildFeedLoader: InboxChildFeedLoader {\n    class Fetcher: InboxFetcher {\n        override func fetchPage(_ page: Int) async throws -> FetchResponse {\n            let response = try await api.getMentionNotifications(\n                page: page,\n                cursor: nil,\n                limit: pageSize,\n                unreadOnly: unreadOnly\n            )\n            return .init(\n                items: response.notifications,\n                prevCursor: nil,\n                nextCursor: response.cursor\n            )\n        }\n\n        override func fetchCursor(_ cursor: String) async throws -> FetchResponse {\n            let response = try await api.getMentionNotifications(\n                page: nil,\n                cursor: cursor,\n                limit: pageSize,\n                unreadOnly: unreadOnly\n            )\n            return .init(\n                items: response.notifications,\n                prevCursor: nil,\n                nextCursor: response.cursor\n            )\n        }\n    }\n    \n    public init(api: ApiClient, pageSize: Int, sortType: FeedLoaderSort.SortType, showRead: Bool) {\n        super.init(\n            api: api,\n            sortType: sortType,\n            fetcher: Fetcher(\n                api: api,\n                pageSize: pageSize,\n                unreadOnly: !showRead\n            ),\n            showRead: showRead\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/InboxFeedLoader/MessageChildFeedLoader.swift",
    "content": "//\n//  MessageChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-26.\n//\n\npublic class MessageChildFeedLoader: InboxChildFeedLoader {\n    class Fetcher: InboxFetcher {\n        override func fetchPage(_ page: Int) async throws -> FetchResponse {\n            let response = try await api.getMessageNotifications(\n                page: page,\n                cursor: nil,\n                limit: pageSize,\n                unreadOnly: unreadOnly\n            )\n            return .init(\n                items: response.notifications,\n                prevCursor: nil,\n                nextCursor: response.cursor\n            )\n        }\n\n        override func fetchCursor(_ cursor: String) async throws -> FetchResponse {\n            let response = try await api.getMessageNotifications(\n                page: nil,\n                cursor: cursor,\n                limit: pageSize,\n                unreadOnly: unreadOnly\n            )\n            return .init(\n                items: response.notifications,\n                prevCursor: nil,\n                nextCursor: response.cursor\n            )\n        }\n    }\n\n    public init(api: ApiClient, pageSize: Int, sortType: FeedLoaderSort.SortType, showRead: Bool) {\n        super.init(\n            api: api,\n            sortType: sortType,\n            fetcher: Fetcher(\n                api: api,\n                pageSize: pageSize,\n                unreadOnly: !showRead\n            ),\n            showRead: showRead\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/InboxFeedLoader/MessageFeedLoader.swift",
    "content": "//\n//  MessageFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-22.\n//\n\nimport Foundation\n\n@Observable\nclass MessageFetcher: Fetcher<Message2> {\n    var personId: Int?\n    \n    init(api: ApiClient, personId: Int?, pageSize: Int) {\n        self.personId = personId\n        \n        super.init(api: api, pageSize: pageSize)\n    }\n    \n    convenience init(person: Person, pageSize: Int) {\n        self.init(api: person.api, personId: person.id, pageSize: pageSize)\n    }\n    \n    override func fetchPage(_ page: Int) async throws -> FetchResponse {\n        let messages = try await api.getMessages(\n            creatorId: personId,\n            page: page,\n            limit: pageSize\n        )\n        \n        return .init(\n            items: messages,\n            prevCursor: nil,\n            nextCursor: nil\n        )\n    }\n}\n\n@Observable\npublic class MessageFeedLoader: StandardFeedLoader<Message2> {\n    public var api: ApiClient\n\n    // force unwrap because this should ALWAYS be a MessageFetcher\n    var messageFetcher: MessageFetcher { fetcher as! MessageFetcher }\n\n    public init(\n        api: ApiClient,\n        personId: Int?,\n        pageSize: Int = 20\n    ) {\n        self.api = api\n\n        super.init(\n            filter: .init(),\n            fetcher: MessageFetcher(\n                api: api,\n                personId: personId,\n                pageSize: pageSize\n            )\n        )\n    }\n    \n    public init(\n        person: Person,\n        pageSize: Int = 20\n    ) {\n        self.api = person.api\n\n        super.init(\n            filter: .init(),\n            fetcher: MessageFetcher(\n                person: person,\n                pageSize: pageSize\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/InboxFeedLoader/ReplyChildFeedLoader.swift",
    "content": "//\n//  ReplyChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-27.\n//\n\npublic class ReplyChildFeedLoader: InboxChildFeedLoader {\n    class Fetcher: InboxFetcher {\n        override func fetchPage(_ page: Int) async throws -> FetchResponse {\n            let response = try await api.getReplyNotifications(\n                page: page,\n                cursor: nil,\n                limit: pageSize,\n                unreadOnly: unreadOnly\n            )\n            return .init(\n                items: response.notifications,\n                prevCursor: nil,\n                nextCursor: response.cursor\n            )\n        }\n\n        override func fetchCursor(_ cursor: String) async throws -> FetchResponse {\n            let response = try await api.getReplyNotifications(\n                page: nil,\n                cursor: cursor,\n                limit: pageSize,\n                unreadOnly: unreadOnly\n            )\n            return .init(\n                items: response.notifications,\n                prevCursor: nil,\n                nextCursor: response.cursor\n            )\n        }\n    }\n\n    public init(api: ApiClient, pageSize: Int, sortType: FeedLoaderSort.SortType, showRead: Bool) {\n        super.init(\n            api: api,\n            sortType: sortType,\n            fetcher: Fetcher(\n                api: api,\n                pageSize: pageSize,\n                unreadOnly: !showRead\n            ),\n            showRead: showRead\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/InboxFeedLoading.swift",
    "content": "//\n//  InboxFeedLoading.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-02-01.\n//\n\nprotocol InboxFeedLoading: FeedLoading {\n    func showRead() async throws\n    func hideRead() async throws\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/InboxIdentifiable.swift",
    "content": "//\n//  InboxIdentifiable.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-02-01.\n//\n\npublic protocol InboxIdentifiable: Equatable {\n    /// Identifier suitable for uniquely distinguishing inbox items from each other\n    var inboxId: Int { get }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/ModMailFeedLoader/ApplicationChildFeedLoader.swift",
    "content": "//\n//  ApplicationChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-12-02.\n//\n\npublic class ApplicationChildFeedLoader: ModMailChildFeedLoader {\n    class Fetcher: ModMailFetcher {\n        override func fetchPage(_ page: Int) async throws -> FetchResponse {\n            guard api.isAdmin else { return .init(items: [], prevCursor: nil, nextCursor: nil) }\n            \n            do {\n                let response = try await api.getRegistrationApplications(page: page, limit: pageSize, unreadOnly: unreadOnly)\n                return .init(\n                    items: response.map { .application($0) },\n                    prevCursor: nil,\n                    nextCursor: nil\n                )\n            } catch let ApiClientError.response(response, _) where response.notAdmin {\n                return .init(items: .init(), prevCursor: nil, nextCursor: nil)\n            }\n        }\n    }\n    \n    public init(api: ApiClient, pageSize: Int, sortType: FeedLoaderSort.SortType, showRead: Bool) {\n        super.init(\n            api: api,\n            sortType: sortType,\n            fetcher: Fetcher(\n                api: api,\n                pageSize: pageSize,\n                unreadOnly: !showRead\n            ),\n            showRead: showRead\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/ModMailFeedLoader/CommentReportChildFeedLoader.swift",
    "content": "//\n//  CommentReportChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-26.\n//\n\npublic class CommentReportChildFeedLoader: ModMailChildFeedLoader {\n    class Fetcher: ModMailFetcher {\n        override func fetchPage(_ page: Int) async throws -> FetchResponse {\n            do {\n                let response = try await api.getCommentReports(page: page, limit: pageSize, unresolvedOnly: unreadOnly)\n                return .init(\n                    items: response.map { .report($0) },\n                    prevCursor: nil,\n                    nextCursor: nil\n                )\n            } catch let ApiClientError.response(response, _) where response.notModOrAdmin {\n                return .init(items: .init(), prevCursor: nil, nextCursor: nil)\n            }\n        }\n    }\n\n    public init(api: ApiClient, pageSize: Int, sortType: FeedLoaderSort.SortType, showRead: Bool) {\n        super.init(\n            api: api,\n            sortType: sortType,\n            fetcher: Fetcher(\n                api: api,\n                pageSize: pageSize,\n                unreadOnly: !showRead\n            ),\n            showRead: showRead\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/ModMailFeedLoader/MessageReportChildFeedLoader.swift",
    "content": "//\n//  MessageReportChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-27.\n//\n\npublic class MessageReportChildFeedLoader: ModMailChildFeedLoader {\n    class Fetcher: ModMailFetcher {\n        override func fetchPage(_ page: Int) async throws -> FetchResponse {\n            guard api.isAdmin else { return .init(items: [], prevCursor: nil, nextCursor: nil) }\n            \n            do {\n                let response = try await api.getMessageReports(page: page, limit: pageSize, unresolvedOnly: unreadOnly)\n                return .init(\n                    items: response.map { .report($0) },\n                    prevCursor: nil,\n                    nextCursor: nil\n                )\n            } catch let ApiClientError.response(response, _) where response.notAdmin {\n                return .init(items: .init(), prevCursor: nil, nextCursor: nil)\n            }\n        }\n    }\n\n    public init(api: ApiClient, pageSize: Int, sortType: FeedLoaderSort.SortType, showRead: Bool) {\n        super.init(\n            api: api,\n            sortType: sortType,\n            fetcher: Fetcher(\n                api: api,\n                pageSize: pageSize,\n                unreadOnly: !showRead\n            ),\n            showRead: showRead\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/ModMailFeedLoader/ModMailChildFeedLoader.swift",
    "content": "//\n//  ModMailChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-12-04.\n//\n\npublic class ModMailChildFeedLoader: ChildFeedLoader<ModMailItem>, InboxFeedLoading {\n    var modMailFetcher: ModMailFetcher { fetcher as! ModMailFetcher }\n    \n    public init(api: ApiClient, sortType: FeedLoaderSort.SortType, fetcher: ModMailFetcher, showRead: Bool) {\n        super.init(filter: ModMailItemFilter(showRead: showRead), fetcher: fetcher, sortType: sortType)\n    }\n    \n    func hideRead() async throws {\n        try await loadingActor.activateFilter(.read) {\n            await setItems(loadingActor.filter.reset(with: items))\n            modMailFetcher.hideRead(unreadCount: items.count)\n            \n            if items.isEmpty {\n                try await refresh(clearBeforeRefresh: false)\n            }\n        }\n    }\n    \n    func showRead() async throws {\n        try await loadingActor.deactivateFilter(.read) {\n            modMailFetcher.showRead()\n            try await refresh(clearBeforeRefresh: true)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/ModMailFeedLoader/ModMailFeedLoader.swift",
    "content": "//\n//  ModMailFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-27.\n//\n\nimport Foundation\n\npublic class ModMailFeedLoader: StandardFeedLoader<ModMailItem> {\n    var modMailFetcher: MultiFetcher<ModMailItem> { fetcher as! MultiFetcher }\n    \n    public init(\n        api: ApiClient,\n        pageSize: Int,\n        sources: [ChildFeedLoader<ModMailItem>],\n        sortType: FeedLoaderSort.SortType,\n        showRead: Bool\n    ) {\n        super.init(\n            filter: ModMailItemFilter(showRead: showRead),\n            fetcher: MultiFetcher(api: api, pageSize: pageSize, sources: sources, sortType: sortType)\n        )\n        \n        for source in sources {\n            source.setParent(parent: self)\n        }\n    }\n    \n    public static func setup(\n        api: ApiClient,\n        pageSize: Int,\n        sortType: FeedLoaderSort.SortType,\n        showRead: Bool\n    ) -> (\n        reportFeedLoader: ReportChildFeedLoader,\n        applicationFeedLoader: ApplicationChildFeedLoader,\n        modMailFeedLoader: ModMailFeedLoader\n    ) {\n        let postReportFeedLoader: PostReportChildFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            showRead: showRead\n        )\n        let commentReportFeedLoader: CommentReportChildFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            showRead: showRead\n        )\n        let messageReportFeedLoader: MessageReportChildFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            showRead: showRead\n        )\n        \n        let reportFeedLoader: ReportChildFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            sources: [postReportFeedLoader, commentReportFeedLoader, messageReportFeedLoader],\n            showRead: showRead\n        )\n        \n        let applicationFeedLoader: ApplicationChildFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            showRead: showRead\n        )\n        \n        let modMailFeedLoader: ModMailFeedLoader = .init(\n            api: api,\n            pageSize: pageSize,\n            sources: [reportFeedLoader, applicationFeedLoader],\n            sortType: sortType,\n            showRead: showRead\n        )\n        \n        return (reportFeedLoader, applicationFeedLoader, modMailFeedLoader)\n    }\n    \n    public func hideRead() async throws {\n        await withThrowingTaskGroup(of: Void.self) { group in\n            for source in modMailFetcher.sources {\n                group.addTask {\n                    guard let childSource = source as? any InboxFeedLoading else {\n                        assertionFailure(\"Child is not ModMailChildFeedLoader\")\n                        return\n                    }\n                    try await childSource.hideRead()\n                }\n            }\n        }\n        \n        try await activateFilter(.read)\n    }\n    \n    public func showRead() async throws {\n        await withThrowingTaskGroup(of: Void.self) { group in\n            for source in modMailFetcher.sources {\n                group.addTask {\n                    guard let childSource = source as? any InboxFeedLoading else {\n                        assertionFailure(\"Child is not ModMailChildFeedLoader\")\n                        return\n                    }\n                    try await childSource.showRead()\n                }\n            }\n        }\n\n        try await deactivateFilter(.read)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/ModMailFeedLoader/ModMailFetcher.swift",
    "content": "//\n//  ModMailFetcher.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-11-24.\n//\n\nimport Foundation\n\n@Observable\npublic class ModMailFetcher: Fetcher<ModMailItem> {\n    var unreadOnly: Bool\n    \n    init(api: ApiClient, pageSize: Int, unreadOnly: Bool) {\n        self.unreadOnly = unreadOnly\n        super.init(api: api, pageSize: pageSize)\n    }\n    \n    /// Updates fetching behavior to hide read items. Assumes items will NOT be cleared from the associated FeedLoader and that deduping will be handled by that FeedLoader.\n    /// - Parameter unreadCount: number of unread items still present after client-side filtering\n    func hideRead(unreadCount: Int) {\n        guard !unreadOnly else {\n            assertionFailure(\"Cannot hide read (unreadOnly already true)\")\n            return\n        }\n        \n        unreadOnly = true\n        page = Int(floor(Double(unreadCount / pageSize)))\n    }\n    \n    /// Updates fetching behavior to show read posts. Assumes associated FeedLoader will immediately perform a refresh.\n    func showRead() {\n        guard unreadOnly else {\n            assertionFailure(\"Cannot show read (unreadOnly already false)\")\n            return\n        }\n        \n        unreadOnly = false\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/ModMailFeedLoader/ModMailItem.swift",
    "content": "//\n//  ModMailItem.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-02-01.\n//\n\npublic enum ModMailItem: FeedLoadable, ReadableProviding, InboxIdentifiable {\n    public typealias FilterType = ModMailItemFilterType\n    \n    case report(Report)\n    case application(RegistrationApplication)\n    \n    var baseValue: any FeedLoadable {\n        switch self {\n        case let .report(report): report\n        case let .application(application): application\n        }\n    }\n    \n    public var read: Bool {\n        switch self {\n        case let .report(report): report.resolved\n        case let .application(application): application.resolution != .unresolved\n        }\n    }\n    \n    public var inboxId: Int {\n        switch self {\n        case let .report(report): report.modMailId\n        case let .application(application): application.modMailId\n        }\n    }\n    \n    public var api: ApiClient { baseValue.api }\n    \n    public func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        baseValue.sortVal(sortType: sortType)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/ModMailFeedLoader/PostReportChildFeedLoader.swift",
    "content": "//\n//  PostReportChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2024-12-02.\n//\n\npublic class PostReportChildFeedLoader: ModMailChildFeedLoader {\n    class Fetcher: ModMailFetcher {\n        override func fetchPage(_ page: Int) async throws -> FetchResponse {\n            do {\n                let response = try await api.getPostReports(page: page, limit: pageSize)\n                return .init(\n                    items: response.map { .report($0) },\n                    prevCursor: nil,\n                    nextCursor: nil\n                )\n            } catch let ApiClientError.response(response, _) where response.notModOrAdmin {\n                return .init(items: .init(), prevCursor: nil, nextCursor: nil)\n            }\n        }\n    }\n    \n    public init(api: ApiClient, pageSize: Int, sortType: FeedLoaderSort.SortType, showRead: Bool) {\n        super.init(\n            api: api,\n            sortType: sortType,\n            fetcher: Fetcher(\n                api: api,\n                pageSize: pageSize,\n                unreadOnly: !showRead\n            ),\n            showRead: showRead\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Inbox/ModMailFeedLoader/ReportChildFeedLoader.swift",
    "content": "//\n//  ReportChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-02-01.\n//\n\nclass ReportFetcher: MultiFetcher<ModMailItem> {}\n\npublic class ReportChildFeedLoader: ChildFeedLoader<ModMailItem>, InboxFeedLoading {\n    var reportFetcher: MultiFetcher<ModMailItem> { fetcher as! ReportFetcher }\n    \n    public init(\n        api: ApiClient,\n        pageSize: Int,\n        sortType: FeedLoaderSort.SortType,\n        sources: [ModMailChildFeedLoader],\n        showRead: Bool\n    ) {\n        let fetcher: ReportFetcher = .init(\n            api: api,\n            pageSize: pageSize,\n            sources: sources,\n            sortType: sortType\n        )\n        \n        super.init(filter: ModMailItemFilter(showRead: showRead), fetcher: fetcher, sortType: sortType)\n        \n        for source in sources {\n            source.setParent(parent: self)\n        }\n    }\n    \n    public func hideRead() async throws {\n        await withThrowingTaskGroup(of: Void.self) { group in\n            for source in reportFetcher.sources {\n                group.addTask {\n                    guard let childSource = source as? ModMailChildFeedLoader else {\n                        assertionFailure(\"Child is not ModMailChildFeedLoader\")\n                        return\n                    }\n                    try await childSource.hideRead()\n                }\n            }\n        }\n        \n        try await activateFilter(.read)\n    }\n    \n    public func showRead() async throws {\n        await withThrowingTaskGroup(of: Void.self) { group in\n            for source in reportFetcher.sources {\n                group.addTask {\n                    guard let childSource = source as? ModMailChildFeedLoader else {\n                        assertionFailure(\"Child is not ModMailChildFeedLoader\")\n                        return\n                    }\n                    try await childSource.showRead()\n                }\n            }\n        }\n\n        try await deactivateFilter(.read)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Modlog/ModlogChildFeedLoader.swift",
    "content": "//\n//  ModlogChildFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-28.\n//\n\nimport Foundation\n\npublic class ModlogChildFeedLoader: ChildFeedLoader<ModlogEntry> {\n    var modlogFetcher: ModlogChildFetcher { fetcher as! ModlogChildFetcher }\n    \n    public init(api: ApiClient, sortType: FeedLoaderSort.SortType, fetcher: ModlogChildFetcher) {\n        super.init(filter: ModlogEntryFilter(), fetcher: fetcher, sortType: sortType)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Modlog/ModlogChildFetcher+SharedCache.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-29.\n//\n\nimport Foundation\n\nextension ModlogChildFetcher {\n    class SharedCache {\n        typealias TaskResponse = [ModlogEntryType: [ModlogEntry]]\n        var api: ApiClient\n        let pageSize: Int\n        var communityId: Int?\n        var targetPersonId: Int?\n        var moderatorPersonId: Int?\n        var ongoingTask: Task<TaskResponse, Error>?\n        \n        init(api: ApiClient, pageSize: Int, communityId: Int?) {\n            self.api = api\n            self.pageSize = pageSize\n            self.communityId = communityId\n        }\n        \n        private func fetchItems() async throws -> TaskResponse {\n            let response = try await api.getModlog(\n                page: 1,\n                limit: pageSize,\n                communityId: communityId,\n                moderatorId: moderatorPersonId,\n                subjectPersonId: targetPersonId\n            )\n            return .init(grouping: response, by: { $0.type.type })\n        }\n        \n        @MainActor\n        func get(type: ModlogEntryType) async throws -> [ModlogEntry] {\n            let task: Task<TaskResponse, Error>\n            if let ongoingTask {\n                task = ongoingTask\n            } else {\n                task = Task { try await fetchItems() }\n                ongoingTask = task\n            }\n            let response = try await task.result.get()\n            return response[type] ?? []\n        }\n        \n        @MainActor\n        func reset() {\n            ongoingTask = nil\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Modlog/ModlogChildFetcher.swift",
    "content": "//\n//  ModlogChildFetcher.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-28.\n//\n\nimport Foundation\n\n@Observable\npublic class ModlogChildFetcher: Fetcher<ModlogEntry> {\n    let sharedCache: SharedCache\n    var communityId: Int?\n    var targetPersonId: Int?\n    var moderatorPersonId: Int?\n    var type: ModlogEntryType\n    \n    init(\n        api: ApiClient,\n        pageSize: Int,\n        sharedCache: SharedCache,\n        communityId: Int?,\n        targetPersonId: Int?,\n        moderatorPersonId: Int?,\n        type: ModlogEntryType\n    ) {\n        self.communityId = communityId\n        self.targetPersonId = targetPersonId\n        self.moderatorPersonId = moderatorPersonId\n        self.type = type\n        self.sharedCache = sharedCache\n        super.init(api: api, pageSize: pageSize)\n    }\n    \n    override func fetchPage(_ page: Int) async throws -> FetchResponse {\n        let items: [ModlogEntry]\n        if page == 1 {\n            items = try await sharedCache.get(type: type)\n        } else {\n            items = try await api.getModlog(\n                page: page,\n                limit: pageSize,\n                communityId: communityId,\n                moderatorId: moderatorPersonId,\n                subjectPersonId: targetPersonId,\n                type: type\n            )\n        }\n        \n        return .init(\n            items: items,\n            prevCursor: nil,\n            nextCursor: nil\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Modlog/ModlogFeedLoader.swift",
    "content": "//\n//  ModlogFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2024-12-28.\n//\n\nimport Foundation\n\npublic class ModlogFeedLoader: StandardFeedLoader<ModlogEntry> {\n    var modlogFetcher: MultiFetcher<ModlogEntry> { fetcher as! MultiFetcher }\n    \n    var modlogSources: [ModlogChildFeedLoader] {\n        (modlogFetcher.sources as? [ModlogChildFeedLoader])!\n    }\n    \n    private var sharedCache: ModlogChildFetcher.SharedCache\n    \n    public init(\n        api: ApiClient,\n        pageSize: Int,\n        communityId: Int?,\n        targetPersonId: Int?,\n        moderatorPersonId: Int?,\n        sortType: FeedLoaderSort.SortType\n    ) {\n        let sharedCache: ModlogChildFetcher.SharedCache = .init(api: api, pageSize: pageSize, communityId: communityId)\n        self.sharedCache = sharedCache\n        \n        let sources: [ModlogChildFeedLoader] = ModlogEntryType.allCases.map { type in\n            .init(\n                api: api,\n                sortType: sortType,\n                fetcher: .init(\n                    api: api,\n                    pageSize: pageSize,\n                    sharedCache: sharedCache,\n                    communityId: communityId,\n                    targetPersonId: targetPersonId,\n                    moderatorPersonId: moderatorPersonId,\n                    type: type\n                )\n            )\n        }\n        super.init(\n            filter: ModlogEntryFilter(),\n            fetcher: MultiFetcher(api: api, pageSize: pageSize, sources: sources, sortType: sortType)\n        )\n        \n        for source in sources {\n            source.setParent(parent: self)\n        }\n    }\n    \n    public func items(ofType type: ModlogEntryType?) -> [ModlogEntry] {\n        if let type {\n            modlogSources.first { $0.modlogFetcher.type == type }?.items ?? []\n        } else {\n            items\n        }\n    }\n    \n    public func childLoader(ofType type: ModlogEntryType) -> ModlogChildFeedLoader {\n        modlogSources.first(where: { $0.modlogFetcher.type == type })!\n    }\n    \n    public func refresh(\n        api: ApiClient? = nil,\n        communityId: Int? = nil,\n        targetPersonId: Int? = nil,\n        moderatorPersonId: Int? = nil,\n        clearBeforeRefresh: Bool = false\n    ) async throws {\n        sharedCache.api = api ?? sharedCache.api\n        sharedCache.communityId = communityId\n        sharedCache.targetPersonId = targetPersonId\n        sharedCache.moderatorPersonId = moderatorPersonId\n        for source in modlogSources {\n            await source.changeApi(to: api ?? sharedCache.api, context: .none())\n            source.modlogFetcher.communityId = communityId ?? source.modlogFetcher.communityId\n            source.modlogFetcher.targetPersonId = targetPersonId ?? source.modlogFetcher.targetPersonId\n            source.modlogFetcher.moderatorPersonId = moderatorPersonId ?? source.modlogFetcher.moderatorPersonId\n        }\n        try await refresh(clearBeforeRefresh: clearBeforeRefresh)\n    }\n    \n    override public func refresh(clearBeforeRefresh: Bool) async throws {\n        await sharedCache.reset()\n        try await super.refresh(clearBeforeRefresh: clearBeforeRefresh)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Person/PersonFeedLoader.swift",
    "content": "//\n//  PersonFeedLoader.swift\n//\n//\n//  Created by Sjmarf on 08/09/2024.\n//\n\nimport Foundation\n\n@Observable\nclass PersonFetcher: Fetcher<Person> {\n    var query: String\n    /// `listing` can be set to `.local` from 0.19.4 onwards.\n    var listing: ListingType\n    var sort: SearchSortType\n    \n    init(api: ApiClient, pageSize: Int, query: String, listing: ListingType, sort: SearchSortType) {\n        self.query = query\n        self.listing = listing\n        self.sort = sort\n        \n        super.init(api: api, pageSize: pageSize)\n    }\n    \n    override func fetchPage(_ page: Int) async throws -> FetchResponse {\n        let communities = try await api.searchPeople(\n            query: query,\n            page: page,\n            limit: pageSize,\n            filter: listing,\n            sort: sort\n        )\n\n        return .init(\n            items: communities,\n            prevCursor: nil,\n            nextCursor: nil\n        )\n    }\n}\n\n@Observable\npublic class PersonFeedLoader: StandardFeedLoader<Person> {\n    public var api: ApiClient\n    \n    // force unwrap because this should ALWAYS be a PersonFetcher\n    var personFetcher: PersonFetcher { fetcher as! PersonFetcher }\n    \n    public init(\n        api: ApiClient,\n        query: String = \"\",\n        pageSize: Int = 20,\n        listing: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) {\n        self.api = api\n        \n        super.init(\n            filter: .init(),\n            fetcher: PersonFetcher(api: api, pageSize: pageSize, query: query, listing: listing, sort: sort)\n        )\n    }\n    \n    public func refresh(\n        query: String? = nil,\n        listing: ListingType? = nil,\n        sort: SearchSortType? = nil,\n        clearBeforeRefresh: Bool = false\n    ) async throws {\n        personFetcher.query = query ?? personFetcher.query\n        personFetcher.listing = listing ?? personFetcher.listing\n        personFetcher.sort = sort ?? personFetcher.sort\n        try await super.refresh(clearBeforeRefresh: clearBeforeRefresh)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Post Feed Loaders/AggregatePostFeedLoader.swift",
    "content": "//\n//  AggregatePostFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-05.\n//\n\nimport Foundation\n\n@Observable\nclass AggregatePostFetcher: PostFetcher {\n    var feedType: ListingType\n    let contentFilter: GetContentFilter?\n    \n    init(api: ApiClient, feedType: ListingType, sortType: PostSortType, pageSize: Int, contentFilter: GetContentFilter?) {\n        self.feedType = feedType\n        self.contentFilter = contentFilter\n        \n        super.init(api: api, sortType: sortType, pageSize: pageSize)\n    }\n    \n    override func getPosts(page: Int, cursor: String?) async throws -> (posts: [Post], cursor: String?) {\n        try await api.getPosts(\n            feed: feedType,\n            sort: sortType,\n            page: page,\n            cursor: cursor,\n            limit: pageSize,\n            filter: contentFilter,\n            showHidden: false // TODO:\n        )\n    }\n}\n\npublic class AggregatePostFeedLoader: CorePostFeedLoader {\n    // force unwrap because this should ALWAYS be an AggregatePostFetcher\n    var aggregatePostFetcher: AggregatePostFetcher { fetcher as! AggregatePostFetcher }\n    \n    // force unwrap because this should ALWAYS be a PostFetcher\n    private var postFetcher: PostFetcher { fetcher as! PostFetcher }\n        \n    public var feedType: ListingType { aggregatePostFetcher.feedType }\n    public var sortType: PostSortType { postFetcher.sortType }\n    \n    public init(\n        pageSize: Int,\n        sortType: PostSortType,\n        showReadPosts: Bool,\n        filterContext: FilterContext,\n        prefetchingConfiguration: PrefetchingConfiguration,\n        urlCache: URLCache,\n        api: ApiClient,\n        feedType: ListingType,\n        contentFilter: GetContentFilter? = nil\n    ) {\n        super.init(\n            showReadPosts: showReadPosts,\n            filterContext: filterContext,\n            prefetchingConfiguration: prefetchingConfiguration,\n            fetcher: AggregatePostFetcher(\n                api: api,\n                feedType: feedType,\n                sortType: sortType,\n                pageSize: pageSize,\n                contentFilter: contentFilter\n            )\n        )\n    }\n    \n    @MainActor\n    public func changeFeedType(to newFeedType: ListingType) async throws {\n        let shouldRefresh = items.isEmpty || aggregatePostFetcher.feedType != newFeedType\n        \n        // always perform assignment--if account changed, feed type will look unchanged but API will be different\n        aggregatePostFetcher.feedType = newFeedType\n        \n        // only refresh if nominal feed type changed\n        if shouldRefresh {\n            try await refresh(clearBeforeRefresh: true)\n        }\n    }\n    \n    /// Changes the post sort type to the specified value and reloads the feed\n    public func changeSortType(to newSortType: PostSortType, forceRefresh: Bool = false) async throws {\n        // don't do anything if sort type not changed\n        guard postFetcher.sortType != newSortType || forceRefresh else {\n            return\n        }\n        \n        postFetcher.sortType = newSortType\n        try await refresh(clearBeforeRefresh: true)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Post Feed Loaders/CommunityPostFeedLoader.swift",
    "content": "//\n//  CommunityPostFeedLoader.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-07.\n//\n\nimport Foundation\n\n@Observable\nclass CommunityPostFetcher: PostFetcher {\n    var community: Community\n    \n    init(sortType: PostSortType, pageSize: Int, community: Community) {\n        self.community = community\n        \n        super.init(api: community.api, sortType: sortType, pageSize: pageSize)\n    }\n    \n    override func getPosts(page: Int, cursor: String?) async throws -> (posts: [Post], cursor: String?) {\n        try await community.getPosts(\n            sort: sortType,\n            page: page,\n            cursor: cursor,\n            limit: pageSize,\n            filter: nil, // TODO:\n            showHidden: false // TODO:\n        )\n    }\n}\n\npublic class CommunityPostFeedLoader: CorePostFeedLoader {\n    public var community: Community\n    \n    var communityPostFetcher: CommunityPostFetcher { fetcher as! CommunityPostFetcher }\n    \n    // force unwrap because this should ALWAYS be a PostFetcher\n    private var postFetcher: PostFetcher { fetcher as! PostFetcher }\n    \n    public var sortType: PostSortType { postFetcher.sortType }\n\n    public init(\n        pageSize: Int,\n        sortType: PostSortType,\n        showReadPosts: Bool,\n        filterContext: FilterContext,\n        prefetchingConfiguration: PrefetchingConfiguration,\n        urlCache: URLCache,\n        community: Community\n    ) {\n        self.community = community\n        super.init(\n            showReadPosts: showReadPosts,\n            filterContext: filterContext,\n            prefetchingConfiguration: prefetchingConfiguration,\n            fetcher: CommunityPostFetcher(sortType: sortType, pageSize: pageSize, community: community)\n        )\n    }\n    \n    override public func changeApi(to newApi: ApiClient, context: FilterContext) async {\n        do {\n            let resolvedCommunity = try await newApi.resolve(url: community.actorId.url)\n            \n            guard let newCommunity = resolvedCommunity as? Community else {\n                assertionFailure(\"Did not get community back\")\n                return\n            }\n            \n            filter.updateContext(to: context)\n            communityPostFetcher.community = newCommunity\n        } catch {\n            assertionFailure(\"Couldn't change API\")\n        }\n    }\n    \n    /// Changes the post sort type to the specified value and reloads the feed\n    public func changeSortType(to newSortType: PostSortType, forceRefresh: Bool = false) async throws {\n        // don't do anything if sort type not changed\n        guard postFetcher.sortType != newSortType || forceRefresh else {\n            return\n        }\n        \n        postFetcher.sortType = newSortType\n        try await refresh(clearBeforeRefresh: true)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Post Feed Loaders/CorePostFeedLoader.swift",
    "content": "//\n//  CorePostFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2026-01-05.\n//\n\nimport Foundation\nimport Nuke\nimport Observation\n\n@Observable\npublic class PostFetcher: Fetcher<Post> {\n    var sortType: PostSortType\n    \n    init(api: ApiClient, sortType: PostSortType, pageSize: Int) {\n        self.sortType = sortType\n        \n        super.init(api: api, pageSize: pageSize)\n    }\n    \n    override func fetchPage(_ page: Int) async throws -> FetchResponse {\n        let result = try await getPosts(page: page, cursor: nil)\n\n        return .init(\n            items: result.posts,\n            prevCursor: nil,\n            nextCursor: result.cursor\n        )\n    }\n    \n    override func fetchCursor(_ cursor: String) async throws -> FetchResponse {\n        let result = try await getPosts(page: 1, cursor: cursor)\n        \n        return .init(\n            items: result.posts,\n            prevCursor: cursor,\n            nextCursor: result.cursor\n        )\n    }\n    \n    func getPosts(page: Int, cursor: String?) async throws -> (posts: [Post], cursor: String?) {\n        preconditionFailure(\"This method must be implemented by the inheriting class\")\n    }\n}\n\n/// Post tracker for use with single feeds. Can easily be extended to load any pure post feed by creating an inheriting class that overrides getPosts().\n@Observable\npublic class CorePostFeedLoader: PrefetchingFeedLoader<Post> {\n    // store reference to the filter used by the LoadingActor so we can modify its filterContext from changeApi\n    var filter: PostFilter\n    \n    init(\n        showReadPosts: Bool,\n        filterContext: FilterContext,\n        prefetchingConfiguration: PrefetchingConfiguration,\n        fetcher: Fetcher<Post>\n    ) {\n        let filter: PostFilter = .init(showRead: showReadPosts, context: filterContext)\n        self.filter = filter\n        \n        super.init(\n            filter: filter,\n            prefetchingConfiguration: prefetchingConfiguration,\n            fetcher: fetcher\n        )\n    }\n    \n    // MARK: Custom Behavior\n\n    override public func changeApi(to newApi: ApiClient, context: FilterContext) async {\n        filter.updateContext(to: context)\n        await fetcher.changeApi(to: newApi, context: context)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Post Feed Loaders/SearchPostFeedLoader.swift",
    "content": "//\n//  SearchPostFeedLoader.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 04/10/2024.\n//\n\nimport Foundation\n\n@Observable\npublic class SearchPostFetcher: Fetcher<Post> {\n    public enum SortType {\n        case v4(SearchSortType)\n        case v3(PostSortType)\n    }\n    \n    public var query: String\n    public var communityId: Int?\n    public var creatorId: Int?\n    \n    public var listing: ListingType\n    \n    public var sortType: SortType\n\n    // setters to allow manual overriding of these for search use cases\n    override public func changeApi(to newApi: ApiClient, context: FilterContext) async {\n        await super.changeApi(to: newApi, context: context)\n    }\n\n    public func setSortType(_ sortType: SortType) { self.sortType = sortType }\n    \n    init(\n        api: ApiClient,\n        sortType: SortType,\n        pageSize: Int,\n        query: String,\n        communityId: Int?,\n        creatorId: Int?,\n        listing: ListingType\n    ) {\n        self.query = query\n        self.communityId = communityId\n        self.creatorId = creatorId\n        self.listing = listing\n        self.sortType = sortType\n        \n        super.init(api: api, pageSize: pageSize)\n    }\n    \n    override func fetchPage(_ page: Int) async throws -> Fetcher<Post>.FetchResponse {\n        let response: [Post]\n        switch sortType {\n        case let .v4(searchSortType):\n            response = try await api.searchPosts(\n                query: query,\n                page: page,\n                limit: pageSize,\n                communityId: communityId,\n                creatorId: creatorId,\n                filter: listing,\n                sort: searchSortType\n            )\n        case let .v3(postSortType):\n            response = try await api.searchPosts(\n                query: query,\n                page: page,\n                limit: pageSize,\n                communityId: communityId,\n                creatorId: creatorId,\n                filter: listing,\n                sort: postSortType\n            )\n        }\n        return .init(items: response, prevCursor: nil, nextCursor: nil)\n    }\n}\n\npublic class SearchPostFeedLoader: CorePostFeedLoader {\n    // force unwrap because this should ALWAYS be a SearchPostFetcher\n    public var searchPostFetcher: SearchPostFetcher { fetcher as! SearchPostFetcher }\n    \n    public init(\n        api: ApiClient,\n        query: String = \"\",\n        pageSize: Int = 20,\n        sortType: SearchPostFetcher.SortType,\n        creatorId: Int? = nil,\n        communityId: Int? = nil,\n        prefetchingConfiguration: PrefetchingConfiguration,\n        urlCache: URLCache,\n        listing: ListingType = .all\n    ) {\n        super.init(\n            showReadPosts: true,\n            filterContext: .none(), // search doesn't filter, only obscures on the frontend\n            prefetchingConfiguration: prefetchingConfiguration,\n            fetcher: SearchPostFetcher(\n                api: api,\n                sortType: sortType,\n                pageSize: pageSize,\n                query: query,\n                communityId: communityId,\n                creatorId: creatorId,\n                listing: listing\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Prefetching/ImagePrefetchProviding.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-25.\n//\n\nimport Foundation\nimport Nuke\n\npublic protocol ImagePrefetchProviding {\n    func imageRequests(configuration config: PrefetchingConfiguration) async -> [ImageRequest]\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/Prefetching/PrefetchingConfiguration.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 10/08/2024.\n//\n\nimport Foundation\nimport Nuke\n\npublic struct PrefetchingConfiguration {\n    public enum ImageResolution {\n        case unlimited, limited(Int)\n    }\n    \n    public var prefetcher: ImagePrefetcher\n    \n    public var imageSize: ImageResolution\n    \n    /// If `nil`, does not fetch avatars.\n    public var avatarSize: Int?\n    \n    let fetchFavicons: Bool\n    let embedLoops: Bool\n    \n    public init(\n        prefetcher: ImagePrefetcher,\n        imageSize: ImageResolution,\n        fetchFavicons: Bool,\n        embedLoops: Bool,\n        avatarSize: Int? = nil\n    ) {\n        self.prefetcher = prefetcher\n        self.imageSize = imageSize\n        self.avatarSize = avatarSize\n        self.fetchFavicons = fetchFavicons\n        self.embedLoops = embedLoops\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/SingleSourceMixedFeedLoader/PersonContent.swift",
    "content": "//\n//  PersonContent.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-21.\n//\n\nimport Foundation\n\npublic class PersonContent: Hashable, Equatable, FeedLoadable, ActorIdentifiable {\n    public typealias FilterType = PersonContentFilterType\n    \n    public let wrappedValue: Value\n    \n    public enum Value {\n        // This always comes from GetPersonDetailsRequest, so we can know we're getting Post2 and Comment2\n        case post(Post)\n        case comment(Comment)\n    }\n    \n    public init(wrappedValue: PersonContent.Value) {\n        self.wrappedValue = wrappedValue\n    }\n    \n    public func sortVal(sortType: FeedLoaderSort.SortType) -> FeedLoaderSort {\n        switch wrappedValue {\n        case let .post(post): post.sortVal(sortType: sortType)\n        case let .comment(comment): comment.sortVal(sortType: sortType)\n        }\n    }\n    \n    public var actorId: ActorIdentifier {\n        switch wrappedValue {\n        case let .post(post): post.actorId\n        case let .comment(comment2): comment2.actorId\n        }\n    }\n    \n    public var api: ApiClient {\n        switch wrappedValue {\n        case let .post(post): post.api\n        case let .comment(comment2): comment2.api\n        }\n    }\n    \n    public func hash(into hasher: inout Hasher) {\n        switch wrappedValue {\n        case let .post(post):\n            hasher.combine(post)\n            hasher.combine(ContentType.post)\n        case let .comment(comment2):\n            hasher.combine(comment2)\n            hasher.combine(ContentType.comment)\n        }\n    }\n    \n    public static func == (lhs: PersonContent, rhs: PersonContent) -> Bool {\n        lhs.actorId == rhs.actorId\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/SingleSourceMixedFeedLoader/PersonContentProviding.swift",
    "content": "//\n//  File.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-23.\n//\n\nimport Foundation\n\n/// Protocol for items that can be converted into a generic PersonContent\npublic protocol PersonContentProviding: FeedLoadable {\n    var userContent: PersonContent { get }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/SingleSourceMixedFeedLoader/PersonContentStream.swift",
    "content": "//\n//  PersonContentStream.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-21.\n//\n\nimport CollectionConcurrencyKit\nimport Foundation\nimport Nuke\n\n// This struct is just a convenience wrapper to handle stream state--all loading operations happen at the FeedLoader level to\n// avoid parent/child concurrency control hell\npublic class PersonContentStream<Item: PersonContentProviding> {\n    // From the frontend it is more ergonomic to have these be PersonContent. These are guaranteed to all be of type Item by\n    // guarding assignment behind `init` and `addItems`, which can only take Item.\n    private(set) var items: [PersonContent]\n    var cursor: Int = 0\n    var doneLoading: Bool = false\n    var thresholds: Thresholds<PersonContent>\n    var prefetchingConfiguration: PrefetchingConfiguration\n    \n    init(items: [Item]? = nil, prefetchingConfiguration: PrefetchingConfiguration) {\n        self.prefetchingConfiguration = prefetchingConfiguration\n        self.thresholds = .init()\n        if let items {\n            let personContentItems: [PersonContent] = items.map(\\.userContent)\n            self.items = personContentItems\n            thresholds.update(with: personContentItems)\n        } else {\n            self.items = .init()\n        }\n    }\n    \n    var needsMoreItems: Bool { !doneLoading && cursor >= items.count }\n    \n    func reset() {\n        items = .init()\n        cursor = 0\n        doneLoading = false\n        thresholds = .init()\n    }\n    \n    func addItems(_ newItems: [Item]) {\n        let personContentItems: [PersonContent] = newItems.map(\\.userContent)\n        preloadImages(personContentItems)\n        items.append(contentsOf: personContentItems)\n        thresholds.update(with: personContentItems)\n        // since the API returns posts and comments together, .success/.done isn't a reliable way to determine whether a particular stream\n        // has finished loading. This will solve that problem in almost every case; however, if the user's filters remove enough items\n        // that the page drops below the threshold, it will erroneously flag the load as done. This would require filtering out 40/50 items,\n        // so it's very unlikely to actually occur.\n        // TODO: 0.19 deprecation rewrite this whole thing with a standard parent/child feed loader setup using the type filtering in /personT\n        if newItems.count < MiddlewareConstants.infiniteLoadThresholdOffset {\n            doneLoading = true\n        }\n    }\n    \n    /// Gets the sort value of the next item in stream for a given sort type without affecting the cursor. Assumes loading has been handled by the FeedLoader.\n    /// - Returns: sorting value of the next tracker item corresponding to the given sort type\n    /// - Warning: This is NOT a thread-safe function! Only one thread at a time per stream may call this function!\n    func nextItemSortVal(sortType: FeedLoaderSort.SortType) async throws -> FeedLoaderSort? {\n        guard cursor < items.count else {\n            return nil\n        }\n        \n        return items[safeIndex: cursor]?.sortVal(sortType: sortType)\n    }\n    \n    /// Gets the next item in the stream and increments the cursor\n    /// - Returns: next item in the feed stream\n    /// - Warning: This is NOT a thread-safe function! Only one thread at a time per stream may call this function!\n    func consumeNextItem() -> PersonContent? {\n        guard cursor < items.count else {\n            return nil\n        }\n        \n        cursor += 1\n        return items[cursor - 1]\n    }\n    \n    /// Preloads images for the given PersonContent items\n    func preloadImages(_ items: [PersonContent]) {\n        Task {\n            // TODO: prefetch comment images\n            let posts = items.compactMap { item in\n                switch item.wrappedValue {\n                case let .post(post):\n                    return post\n                default: return nil\n                }\n            }\n            \n            await prefetchingConfiguration.prefetcher.startPrefetching(with: posts.concurrentFlatMap { post -> [ImageRequest] in\n                await post.imageRequests(configuration: self.prefetchingConfiguration)\n            })\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/FeedLoaders/SingleSourceMixedFeedLoader/SingleSourceMixedFeedLoader.swift",
    "content": "//\n//  SingleSourceMixedFeedLoader.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-09.\n//\n\nimport Foundation\nimport Nuke\nimport Semaphore\n\n/// This is a special type of FeedLoader built for user content, which is uniquely challenging because you cannot load\n/// just posts or just comments, and thus the standard Parent/Child FeedLoader construction does not work without\n/// severe API waste. This solution is a simplified variant of that architecture.\n///\n/// The SingleSourceMixedFeedLoader is the parent loader. It is responsible for all data fetching, and keeps track of two\n/// PersonContentStreams, one for Posts and one for Comments. To load a page of items, it consumes and merges the child streams, just as\n/// in the standard Parent/Child FeedLoader; if either stream reaches the end of its items, it triggers a new load, the response from\n/// which is then incorporated into both child streams.\n\n@Observable\nclass SingleSourceMixedFetcher: Fetcher<PersonContent> {\n    var sortType: FeedLoaderSort.SortType\n    var userId: Int\n    var savedOnly: Bool\n    \n    var postStream: PersonContentStream<Post>\n    var commentStream: PersonContentStream<Comment>\n    \n    init(\n        api: ApiClient,\n        pageSize: Int,\n        sortType: FeedLoaderSort.SortType,\n        userId: Int,\n        savedOnly: Bool,\n        withContent: (posts: [Post], comments: [Comment])?,\n        prefetchingConfiguration: PrefetchingConfiguration\n    ) {\n        self.sortType = sortType\n        self.userId = userId\n        self.savedOnly = savedOnly\n        self.postStream = .init(items: withContent?.posts, prefetchingConfiguration: prefetchingConfiguration)\n        self.commentStream = .init(items: withContent?.comments, prefetchingConfiguration: prefetchingConfiguration)\n        \n        super.init(api: api, pageSize: pageSize, page: withContent == nil ? 0 : 1)\n    }\n    \n    override func reset() async {\n        postStream.reset()\n        commentStream.reset()\n        \n        await super.reset()\n    }\n    \n    override func fetch() async throws -> LoadingResponse<PersonContent> {\n        var newItems: [PersonContent] = .init()\n        \n        while newItems.count < pageSize {\n            if let nextItem = try await computeNextItem() {\n                newItems.append(nextItem)\n            } else {\n                return .done(newItems)\n            }\n        }\n        \n        return .success(newItems)\n    }\n    \n    override func fetchPage(_ page: Int) async throws -> FetchResponse {\n        fatalError(\"Unsupported loading operation\")\n    }\n    \n    override func fetchCursor(_ cursor: String) async throws -> FetchResponse {\n        fatalError(\"Unsupported loading operation\")\n    }\n    \n    /// Returns the next post or comment, depending on which is sorted first\n    private func computeNextItem() async throws -> PersonContent? {\n        // if either postStream or commentStream needs items, load the next page from the API\n        if postStream.needsMoreItems || commentStream.needsMoreItems {\n            page += 1\n            let response = try await api.getContent(authorId: userId, sort: .new, page: page, limit: pageSize, savedOnly: savedOnly)\n            postStream.addItems(response.posts)\n            commentStream.addItems(response.comments)\n        }\n        \n        let nextPost = try await postStream.nextItemSortVal(sortType: sortType)\n        let nextComment = try await commentStream.nextItemSortVal(sortType: sortType)\n        \n        if let nextPost {\n            if let nextComment {\n                // if both next post and next comment, return higher sort\n                return nextPost > nextComment ? postStream.consumeNextItem() : commentStream.consumeNextItem()\n            } else {\n                // if next post but no next comment, return next post\n                return postStream.consumeNextItem()\n            }\n        }\n        \n        // if no next post, always return next comment (this returns nil if no next comment)\n        return commentStream.consumeNextItem()\n    }\n}\n\npublic class SingleSourceMixedFeedLoader: StandardFeedLoader<PersonContent> {\n    // force unwrap because this should ALWAYS be a SingleSourceMixedFetcher\n    var singleSourceMixedFetcher: SingleSourceMixedFetcher { fetcher as! SingleSourceMixedFetcher }\n    \n    public var api: ApiClient { singleSourceMixedFetcher.api }\n    public var userId: Int { singleSourceMixedFetcher.userId }\n    \n    // MARK: Custom Behavior\n\n    // This FeedLoader is slightly awkward because it functions like a multi-loader but draws its posts and comments from a single API call. The streams act essentially like child loaders, but are populated using custom behavior in the fetcher. This FeedLoader is best understood as a multi-loader with the streams as child loaders.\n    \n    private var postStream: PersonContentStream<Post> { singleSourceMixedFetcher.postStream }\n    private var commentStream: PersonContentStream<Comment> { singleSourceMixedFetcher.commentStream }\n    \n    // these are used to allow refresh without clear\n    private var tempPostStream: PersonContentStream<Post>?\n    private var tempCommentStream: PersonContentStream<Comment>?\n    \n    // convenience accessors for child types\n    public var posts: [PersonContent] { tempPostStream?.items ?? postStream.items }\n    public var postLoadingState: FeedLoadingState { postStream.doneLoading ? .done : loadingState }\n    \n    public var comments: [PersonContent] { tempCommentStream?.items ?? commentStream.items }\n    public var commentLoadingState: FeedLoadingState { commentStream.doneLoading ? .done : loadingState }\n    \n    public init(\n        api: ApiClient,\n        pageSize: Int,\n        userId: Int,\n        sortType: FeedLoaderSort.SortType,\n        savedOnly: Bool,\n        prefetchingConfiguration: PrefetchingConfiguration,\n        withContent: (posts: [Post], comments: [Comment])? = nil\n    ) {\n        super.init(filter: MultiFilter(), fetcher: SingleSourceMixedFetcher(\n            api: api,\n            pageSize: pageSize,\n            sortType: sortType,\n            userId: userId,\n            savedOnly: savedOnly,\n            withContent: withContent,\n            prefetchingConfiguration: prefetchingConfiguration\n        ))\n    }\n    \n    // MARK: Custom Behavior\n    \n    override public func refresh(clearBeforeRefresh: Bool) async throws {\n        if !clearBeforeRefresh {\n            tempPostStream = postStream\n            tempCommentStream = commentStream\n        }\n        \n        try await super.refresh(clearBeforeRefresh: clearBeforeRefresh)\n        \n        tempPostStream = nil\n        tempCommentStream = nil\n    }\n    \n    public func changeUser(api: ApiClient, context: FilterContext, userId: Int) async {\n        tempPostStream = postStream\n        tempCommentStream = commentStream\n        \n        await singleSourceMixedFetcher.changeApi(to: api, context: context)\n        singleSourceMixedFetcher.userId = userId\n        await loadingActor.reset()\n        await setLoading(.done) // prevent loading more items until refreshed\n    }\n    \n    public func loadIfThreshold(_ item: PersonContent, asChild: Bool) throws {\n        let shouldLoad: Bool\n        if asChild {\n            shouldLoad = switch item.wrappedValue {\n            case .post: postStream.thresholds.isThreshold(item)\n            case .comment: commentStream.thresholds.isThreshold(item)\n            }\n        } else {\n            shouldLoad = thresholds.isThreshold(item)\n        }\n        \n        // regardless of which threshold triggers this, always call loadMoreItems() because there's no item-specific endpoint\n        if shouldLoad {\n            Task(priority: .userInitiated) {\n                try await loadMoreItems()\n            }\n        }\n    }\n    \n    public func setPrefetchingConfiguration(_ config: PrefetchingConfiguration) {\n        postStream.prefetchingConfiguration = config\n        commentStream.prefetchingConfiguration = config\n        \n        postStream.preloadImages(items)\n        // note that this currently doesn't do anything because comments don't support prefetching yet [Eric 2024.11.13]\n        commentStream.preloadImages(items)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/InstanceConnection.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-05.\n//\n\nimport Foundation\n\npublic protocol InstanceConnection {\n    static var softwareType: SiteSoftwareType { get }\n    \n    init(baseUrl: URL, token: String?)\n        \n    func updateToken(_ newToken: String)\n    \n    var contextIsFetched: Bool { get }\n    func supports(_ feature: Feature) async throws -> Bool\n    func supports(_ feature: Feature, defaultValue: Bool) -> Bool\n\n    var fetchedVersion: SiteVersion? { get }\n    var version: SiteVersion { get async throws }\n    var myPersonId: Int? { get async throws }\n    func ensureContextPresence() async throws\n\n    // MARK: - Post\n    \n    func getPosts(\n        communityId: Int,\n        sort: PostSortType,\n        page: Int,\n        cursor: String?,\n        limit: Int,\n        filter: GetContentFilter?,\n        showHidden: Bool\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?)\n    \n    func getPosts(\n        feed: ListingType,\n        sort: PostSortType,\n        page: Int,\n        cursor: String?,\n        limit: Int,\n        filter: GetContentFilter?,\n        showHidden: Bool\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?)\n        \n    func getPosts(\n        personId: Int,\n        communityId: Int?,\n        sort: PostSortType,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool\n    ) async throws -> (person: Person3Snapshot, posts: [Post2Snapshot])\n        \n    func getPostHistory(\n        type: GetContentFilter,\n        page: Int?,\n        cursor: String?,\n        limit: Int\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?)\n\n    func getPost(id: Int) async throws -> Post3Snapshot\n    func getPost(url: URL) async throws -> Post2Snapshot\n    \n    // This method should be removed in favor of the below method once we drop support for versions before Lemmy 1.0\n    func searchPosts(\n        query: String,\n        page: Int,\n        limit: Int,\n        communityId: Int?,\n        creatorId: Int?,\n        filter: ListingType,\n        sort: PostSortType\n    ) async throws -> [Post2Snapshot]\n    \n    func searchPosts(\n        query: String,\n        page: Int,\n        limit: Int,\n        communityId: Int?,\n        creatorId: Int?,\n        filter: ListingType,\n        sort: SearchSortType\n    ) async throws -> [Post2Snapshot]\n    \n    func markPostsAsRead(ids: Set<Int>, read: Bool) async throws\n    func markPostAsRead(id: Int, read: Bool) async throws\n    @discardableResult\n    func voteOnPost(id: Int, score: ScoringOperation) async throws -> Post2Snapshot\n    @discardableResult\n    func savePost(id: Int, save: Bool) async throws -> Post2Snapshot\n    @discardableResult\n    func deletePost(id: Int, delete: Bool) async throws -> Post2Snapshot\n    func hidePost(id: Int, hide: Bool) async throws\n    \n    func createPost(\n        communityId: Int,\n        title: String,\n        content: String?,\n        linkUrl: URL?,\n        altText: String?,\n        thumbnail: URL?,\n        nsfw: Bool,\n        languageId: Int?\n    ) async throws -> Post2Snapshot\n    \n    @discardableResult\n    func editPost(\n        id: Int,\n        title: String,\n        content: String?,\n        linkUrl: URL?,\n        altText: String?,\n        thumbnail: URL?,\n        nsfw: Bool,\n        languageId: Int?\n    ) async throws -> Post2Snapshot\n    \n    func replyToPost(\n        id: Int,\n        content: String,\n        languageId: Int?\n    ) async throws -> Comment2Snapshot\n    \n    @discardableResult\n    func reportPost(id: Int, reason: String) async throws -> ReportSnapshot\n    func purgePost(id: Int, reason: String?) async throws\n    \n    @discardableResult\n    func removePost(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Post2Snapshot\n    \n    @discardableResult\n    func pinPost(\n        id: Int,\n        pin: Bool,\n        to target: PostFeatureType\n    ) async throws -> Post2Snapshot\n    \n    @discardableResult\n    func lockPost(id: Int, lock: Bool) async throws -> Post2Snapshot\n    \n    @discardableResult\n    func setPostNsfw(id: Int, nsfw: Bool) async throws -> Post1Snapshot\n    \n    @discardableResult\n    func getPostVotes(\n        id: Int,\n        page: Int,\n        limit: Int\n    ) async throws -> [PersonVoteSnapshot]\n\n    @discardableResult\n    func voteInPoll(postId: Int, choiceIds: Set<Int>) async throws -> Post2Snapshot \n    \n    // MARK: - Comment\n    \n    func getComment(id: Int) async throws -> Comment2Snapshot\n    func getComment(url: URL) async throws -> Comment2Snapshot\n\n    func getComments(\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int?,\n        limit: Int,\n        filter: GetContentFilter?\n    ) async throws -> [Comment2Snapshot]\n    \n    func getComments(\n        postId: Int,\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int?,\n        limit: Int,\n        filter: GetContentFilter?\n    ) async throws -> [Comment2Snapshot]\n    \n    func getComments(\n        parentId: Int,\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int?,\n        limit: Int,\n        filter: GetContentFilter?\n    ) async throws -> [Comment2Snapshot]\n\n    func getCommentHistory(\n        type: GetContentFilter,\n        page: Int?,\n        cursor: String?,\n        limit: Int\n    ) async throws -> (comments: [Comment2Snapshot], cursor: String?)\n    \n    // This method should be removed in favor of the below method once we drop support for versions before Lemmy 1.0\n    func searchComments(\n        query: String,\n        page: Int,\n        limit: Int,\n        communityId: Int?,\n        creatorId: Int?,\n        filter: ListingType,\n        sort: CommentSortType\n    ) async throws -> [Comment2Snapshot]\n    \n    func searchComments(\n        query: String,\n        page: Int,\n        limit: Int,\n        communityId: Int?,\n        creatorId: Int?,\n        filter: ListingType,\n        sort: SearchSortType\n    ) async throws -> [Comment2Snapshot]\n    \n    @discardableResult\n    func voteOnComment(id: Int, score: ScoringOperation) async throws -> Comment2Snapshot\n    @discardableResult\n    func saveComment(id: Int, save: Bool) async throws -> Comment2Snapshot\n    @discardableResult\n    func deleteComment(id: Int, delete: Bool) async throws -> Comment2Snapshot\n    \n    @discardableResult\n    func editComment(\n        id: Int,\n        content: String,\n        languageId: Int?\n    ) async throws -> Comment2Snapshot\n    \n    func replyToComment(postId: Int, parentId: Int?, content: String, languageId: Int?) async throws -> Comment2Snapshot\n    \n    @discardableResult\n    func reportComment(id: Int, reason: String) async throws -> ReportSnapshot\n    func purgeComment(id: Int, reason: String?) async throws\n    \n    @discardableResult\n    func removeComment(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Comment2Snapshot\n    \n    @discardableResult\n    func getCommentVotes(\n        id: Int,\n        page: Int,\n        limit: Int\n    ) async throws -> [PersonVoteSnapshot]\n    \n    // MARK: - Person\n    \n    func getPerson(id: Int) async throws -> Person3Snapshot\n    func getPerson(url: URL) async throws -> Person2Snapshot\n    func getPerson(username: String) async throws -> Person3Snapshot\n    \n    func searchPeople(\n        query: String,\n        page: Int,\n        limit: Int,\n        filter: ListingType,\n        sort: SearchSortType\n    ) async throws -> [Person2Snapshot]\n    \n    @discardableResult\n    func blockPerson(id: Int, block: Bool) async throws -> Person2Snapshot\n    \n    @discardableResult\n    func banPersonFromCommunity(\n        personId: Int,\n        communityId: Int,\n        ban: Bool,\n        removeContent: Bool,\n        reason: String?,\n        expires: Date?\n    ) async throws -> Person1Snapshot\n    \n    @discardableResult\n    func banPersonFromInstance(\n        personId: Int,\n        ban: Bool,\n        removeContent: Bool,\n        reason: String?,\n        expires: Date?\n    ) async throws -> Person2Snapshot\n    \n    func purgePerson(id: Int, reason: String?) async throws\n    \n    func getContent(\n        authorId id: Int,\n        sort: PostSortType,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool?,\n        communityId: Int?\n    ) async throws -> (person: Person3Snapshot, posts: [Post2Snapshot], comments: [Comment2Snapshot])\n    \n    func getMyPerson() async throws -> (person: Person4Snapshot?, instance: Instance3Snapshot, blocks: BlockListSnapshot?)\n    func deleteAccount(password: String, deleteContent: Bool) async throws\n\n    func editNote(id: Int, content: String?) async throws\n\n    func editProfile(details: ProfileDetails) async throws\n    \n    func editAccountSettings(\n        showNsfw: Bool?,\n        showScores: Bool?,\n        theme: String?,\n        defaultListingType: ListingType?,\n        interfaceLanguage: String?,\n        avatar: String?,\n        banner: String?,\n        displayName: String?,\n        email: String?,\n        bio: String?,\n        matrixUserId: String?,\n        showAvatars: Bool?,\n        sendNotificationsToEmail: Bool?,\n        botAccount: Bool?,\n        showBotAccounts: Bool?,\n        showReadPosts: Bool?,\n        discussionLanguages: [Int]?,\n        openLinksInNewTab: Bool?,\n        blurNsfw: Bool?,\n        autoExpand: Bool?,\n        infiniteScrollEnabled: Bool?,\n        postListingMode: PostFeedViewMode?,\n        enableKeyboardNavigation: Bool?,\n        enableAnimatedImages: Bool?,\n        collapseBotComments: Bool?,\n        showUpvotes: Bool?,\n        showDownvotes: Bool?,\n        showUpvotePercentage: Bool?\n    ) async throws\n    \n    // MARK: - Community\n\n    func getCommunity(id: Int) async throws -> Community3Snapshot\n    func getCommunity(url: URL) async throws -> Community2Snapshot\n\n    func editCommunityDescription(id: Int, newValue: String?) async throws -> Community2Snapshot \n    \n    func searchCommunities(\n        query: String,\n        page: Int,\n        limit: Int,\n        filter: ListingType,\n        sort: SearchSortType\n    ) async throws -> [Community2Snapshot]\n    \n    @discardableResult\n    func getSubscriptionList(page: Int, limit: Int) async throws -> [Community2Snapshot]\n    @discardableResult\n    func subscribeToCommunity(id: Int, subscribe: Bool) async throws -> Community2Snapshot\n    @discardableResult\n    func blockCommunity(id: Int, block: Bool) async throws -> Community2Snapshot\n    \n    @discardableResult\n    func removeCommunity(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Community2Snapshot\n    \n    func purgeCommunity(id: Int, reason: String?) async throws\n    \n    @discardableResult\n    func addModerator(\n        communityId: Int,\n        personId: Int,\n        added: Bool\n    ) async throws -> (moderators: [Person1Snapshot], community: Community1Snapshot)\n    \n    // MARK: - General\n\n    func getAccountToken(usernameOrEmail: String, password: String, totpToken: String?) async throws -> String\n    func getUsernameFromToken(token: String) async throws -> String\n    \n    func signUp(\n        username: String,\n        password: String,\n        confirmPassword: String,\n        showNsfw: Bool,\n        email: String?,\n        captcha: Captcha?,\n        captchaAnswer: String?,\n        applicationQuestionResponse: String?\n    ) async throws -> SignUpResponse\n    \n    @discardableResult\n    func changePassword(\n        newPassword: String,\n        confirmNewPassword: String,\n        oldPassword: String\n    ) async throws -> String\n    \n    func getCaptcha() async throws -> Captcha\n    \n    func resolve(url: URL) async throws -> ResolvedContent\n    \n    func getBlocked() async throws -> (people: [Person1Snapshot], communities: [Community1Snapshot], instances: [Instance1Snapshot])\n    \n    func getModlog(\n        page: Int,\n        limit: Int,\n        communityId: Int?,\n        moderatorId: Int?,\n        subjectPersonId: Int?,\n        postId: Int?,\n        commentId: Int?,\n        type: ModlogEntryType?\n    ) async throws -> [ModlogEntrySnapshot]\n    \n    func getPostLink(url: URL) async throws -> PostLink\n\n    // MARK: - Inbox\n    \n    func getMessages(\n        creatorId: Int?,\n        page: Int,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> [Message2Snapshot]\n    \n    func getReplyNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?)\n\n    func getMentionNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?)\n\n    func getMessageNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?)\n\n    func markNotificationAsRead(\n        type: InboxNotificationContentType,\n        id: Int,\n        contentId: Int,\n        read: Bool\n    ) async throws\n        \n    func markAllAsRead() async throws\n    func getPersonalUnreadCount() async throws -> PersonalUnreadCountSnapshot\n    func createMessage(personId: Int, content: String) async throws -> Message2Snapshot\n    @discardableResult\n    func editMessage(id: Int, content: String) async throws -> Message2Snapshot\n    @discardableResult\n    func reportMessage(id: Int, reason: String) async throws -> ReportSnapshot\n    @discardableResult\n    func deleteMessage(id: Int, delete: Bool) async throws -> Message2Snapshot\n    \n    // MARK: - Instance\n    \n    func getMyInstance() async throws -> Instance3Snapshot\n    func getFederatedInstances() async throws -> FederationPolicy\n    func blockInstance(instanceId: Int, block: Bool) async throws\n    @discardableResult\n    func addAdmin(personId: Int, added: Bool) async throws -> [Person2Snapshot]\n    \n    // MARK: - RegistrationApplication\n    \n    func getRegistrationApplicationCount() async throws -> Int\n    \n    func getRegistrationApplications(\n        page: Int,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> [RegistrationApplicationSnapshot]\n    \n    @discardableResult\n    func approveRegistrationApplication(id: Int) async throws -> RegistrationApplicationSnapshot\n    @discardableResult\n    func denyRegistrationApplication(id: Int, reason: String?) async throws -> RegistrationApplicationSnapshot\n    \n    // MARK: - Report\n    \n    func getReportCount(communityId: Int?) async throws -> ReportUnreadCountSnapshot\n    \n    func getPostReports(\n        page: Int,\n        limit: Int,\n        unresolvedOnly: Bool,\n        communityId: Int?,\n        postId: Int?\n    ) async throws -> [ReportSnapshot]\n    \n    func getCommentReports(\n        page: Int,\n        limit: Int,\n        unresolvedOnly: Bool,\n        communityId: Int?,\n        commentId: Int?\n    ) async throws -> [ReportSnapshot]\n    \n    func getMessageReports(\n        page: Int,\n        limit: Int,\n        unresolvedOnly: Bool\n    ) async throws -> [ReportSnapshot]\n    \n    @discardableResult\n    func resolvePostReport(id: Int, resolved: Bool) async throws -> ReportSnapshot\n    @discardableResult\n    func resolveCommentReport(id: Int, resolved: Bool) async throws -> ReportSnapshot\n    @discardableResult\n    func resolveMessageReport(id: Int, resolved: Bool) async throws -> ReportSnapshot\n    \n    // MARK: - Image\n    \n    func uploadImage(\n        _ imageData: Data,\n        fileExtension: String,\n        onProgress progressCallback: @escaping (_ progress: Double) -> Void\n    ) async throws -> ImageUpload1Snapshot\n    \n    func deleteImage(alias: String, deleteToken: String) async throws\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+Comment.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-06.\n//\n\nimport Foundation\n\npublic extension LemmyConnection {\n    func getComment(id: Int) async throws -> Comment2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyGetCommentRequest(endpoint: endpoint, id: id)\n        }\n        return try .init(from: response.commentView)\n    }\n    \n    func getComment(url: URL) async throws -> Comment2Snapshot {\n        do {\n            let result = try await resolve(url: url)\n            switch result {\n            case let .comment(comment):\n                return comment\n            default:\n                throw ApiClientError.noEntityFound\n            }\n        } catch let ApiClientError.response(response, _) where response.couldntFindObject {\n            throw ApiClientError.noEntityFound\n        }\n    }\n    \n    func getComments(\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int?,\n        limit: Int,\n        filter: GetContentFilter?\n    ) async throws -> [Comment2Snapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyListCommentsRequest(\n                endpoint: endpoint,\n                type_: .all,\n                sort: sort.v3CommentApiType,\n                maxDepth: maxDepth,\n                page: page,\n                limit: limit,\n                communityId: nil,\n                communityName: nil,\n                postId: nil,\n                parentId: nil,\n                savedOnly: filter == .saved,\n                likedOnly: filter == .upvoted,\n                dislikedOnly: filter == .downvoted,\n                timeRangeSeconds: sort.timeRangeSeconds,\n                pageCursor: nil,\n                searchTerm: nil\n            )\n        }\n        return try response.items.map { try .init(from: $0) }\n    }\n\n    func getComments(\n        postId: Int,\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment2Snapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyListCommentsRequest(\n                endpoint: endpoint,\n                type_: .all,\n                sort: sort.v3CommentApiType,\n                maxDepth: maxDepth,\n                page: page,\n                limit: limit,\n                communityId: nil,\n                communityName: nil,\n                postId: postId,\n                parentId: nil,\n                savedOnly: filter == .saved,\n                likedOnly: filter == .upvoted,\n                dislikedOnly: filter == .downvoted,\n                timeRangeSeconds: sort.timeRangeSeconds,\n                pageCursor: nil,\n                searchTerm: nil\n            )\n        }\n        return try response.items.map { try .init(from: $0) }\n    }\n    \n    func getComments(\n        parentId: Int,\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment2Snapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyListCommentsRequest(\n                endpoint: endpoint,\n                type_: .all,\n                sort: sort.v3CommentApiType,\n                maxDepth: maxDepth,\n                page: page,\n                limit: limit,\n                communityId: nil,\n                communityName: nil,\n                postId: nil,\n                parentId: parentId,\n                savedOnly: filter == .saved,\n                likedOnly: filter == .upvoted,\n                dislikedOnly: filter == .downvoted,\n                timeRangeSeconds: sort.timeRangeSeconds,\n                pageCursor: nil,\n                searchTerm: nil\n            )\n        }\n        return try response.items.map { try .init(from: $0) }\n    }\n\n    func getCommentHistory(\n        type: GetContentFilter,\n        page: Int?,\n        cursor: String?,\n        limit: Int\n    ) async throws -> (comments: [Comment2Snapshot], cursor: String?) {\n        try await processingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                guard let page else {\n                    throw ApiClientError.featureUnsupported\n                }\n\n                let request = LemmyListCommentsRequest(\n                    endpoint: .v3,\n                    type_: .all,\n                    sort: .new,\n                    maxDepth: nil,\n                    page: page,\n                    limit: limit,\n                    communityId: nil,\n                    communityName: nil,\n                    postId: nil,\n                    parentId: nil,\n                    savedOnly: type == .saved,\n                    likedOnly: type == .upvoted,\n                    dislikedOnly: type == .downvoted,\n                    timeRangeSeconds: nil,\n                    pageCursor: nil,\n                    searchTerm: nil\n                )\n                let response = try await self.perform(request, endpoint: .v3)\n                return try (\n                    comments: response.items.map { try .init(from: $0) },\n                    cursor: response.nextPage\n                )\n            case .v4:\n                switch type {\n                case .saved:\n                let request = LemmyListPersonSavedRequest(\n                    type_: .comments,\n                    pageCursor: cursor,\n                    limit: limit\n                )\n                let response = try await self.perform(request, endpoint: .v4)\n                return try (\n                    comments: response.items.compactMap(\\.commentValue).map {\n                        try .init(from: $0)\n                    },\n                    cursor: response.nextPage\n                )\n                default:\n                let request = LemmyListPersonLikedRequest(\n                    type_: .comments,\n                    likeType: type == .upvoted ? .likedOnly : .dislikedOnly,\n                    pageCursor: cursor,\n                    limit: limit\n                )\n                let response = try await self.perform(request, endpoint: .v4)\n                return try (\n                    comments: response.items.compactMap(\\.commentValue).map {\n                        try .init(from: $0)\n                    },\n                    cursor: response.nextPage\n                )\n                }\n            }\n        }\n    }\n    \n    // This method should be removed in favor of the below method once we drop support for versions before Lemmy 1.0\n    func searchComments(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: CommentSortType = .top(.allTime)\n    ) async throws -> [Comment2Snapshot] {\n        try await searchComments(\n            query: query,\n            page: page,\n            limit: limit,\n            communityId: communityId,\n            creatorId: creatorId,\n            filter: filter,\n            createSortType: { try sort.apiType(for: $0) },\n            timeRangeSeconds: sort.timeRangeSeconds\n        )\n    }\n    \n    func searchComments(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) async throws -> [Comment2Snapshot] {\n        try await searchComments(\n            query: query,\n            page: page,\n            limit: limit,\n            communityId: communityId,\n            creatorId: creatorId,\n            filter: filter,\n            createSortType: { try sort.apiType(for: $0) },\n            timeRangeSeconds: sort.timeRangeSeconds\n        )\n    }\n\n    private func searchComments(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int?,\n        creatorId: Int?,\n        filter: ListingType,\n        createSortType: @escaping (LemmyEndpointVersion) throws -> LemmySearchSortTypeBridge,\n        timeRangeSeconds: Int?\n    ) async throws -> [Comment2Snapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            try LemmySearchRequest(\n                endpoint: endpoint,\n                q: query,\n                communityId: communityId,\n                communityName: nil,\n                creatorId: creatorId,\n                type_: .comments,\n                sort: createSortType(endpoint),\n                listingType: filter.apiType,\n                page: page,\n                limit: limit,\n                postTitleOnly: false,\n                searchTerm: query,\n                searchTitleOnly: false\n            )\n        }\n        return try response.comments.map { try .init(from: $0) }\n    }\n    \n    @discardableResult\n    func voteOnComment(id: Int, score: ScoringOperation) async throws -> Comment2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyLikeCommentRequest(\n                endpoint: endpoint,\n                commentId: id,\n                score: score.rawValue,\n                isUpvote: score.booleanValue\n            )\n        }\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func saveComment(id: Int, save: Bool) async throws -> Comment2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmySaveCommentRequest(endpoint: endpoint, commentId: id, save: save)\n        }\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func deleteComment(id: Int, delete: Bool) async throws -> Comment2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyDeleteCommentRequest(endpoint: endpoint, commentId: id, deleted: delete)\n        }\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func editComment(\n        id: Int,\n        content: String,\n        languageId: Int?\n    ) async throws -> Comment2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyEditCommentRequest(\n                endpoint: endpoint,\n                commentId: id,\n                content: content,\n                languageId: languageId\n            )\n        }\n        return try .init(from: response.commentView)\n    }\n    \n    func replyToComment(postId: Int, parentId: Int?, content: String, languageId: Int? = nil) async throws -> Comment2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyCreateCommentRequest(\n                endpoint: endpoint,\n                content: content,\n                postId: postId,\n                parentId: parentId,\n                languageId: languageId\n            )\n        }\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func reportComment(id: Int, reason: String) async throws -> ReportSnapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyCreateCommentReportRequest(\n                endpoint: endpoint,\n                commentId: id,\n                reason: reason,\n                violatesInstanceRules: nil\n            )\n        }\n        return try .init(from: response.commentReportView)\n    }\n    \n    func purgeComment(id: Int, reason: String?) async throws {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyPurgeCommentRequest(endpoint: endpoint, commentId: id, reason: reason)\n        }\n        guard response.success else { throw ApiClientError.unsuccessful }\n    }\n    \n    @discardableResult\n    func removeComment(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Comment2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyRemoveCommentRequest(\n                endpoint: endpoint,\n                commentId: id,\n                removed: remove,\n                reason: reason,\n                removeChildren: nil\n            )\n        }\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func getCommentVotes(\n        id: Int,\n        page: Int = 1,\n        limit: Int = 20\n    ) async throws -> [PersonVoteSnapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyListCommentLikesRequest(\n                endpoint: endpoint,\n                commentId: id,\n                page: page,\n                limit: limit,\n                pageCursor: nil\n            )\n        }\n        return try response.items.map { try .init(from: $0) }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+Community.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-07.\n//\n\nimport Foundation\n\npublic extension LemmyConnection {\n    func getCommunity(id: Int) async throws -> Community3Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyGetCommunityRequest(endpoint: endpoint, id: id, name: nil)\n        }\n        return try .init(from: response)\n    }\n    \n    func getCommunity(url: URL) async throws -> Community2Snapshot {\n        do {\n            let result = try await resolve(url: url)\n            switch result {\n            case let .community(community):\n                return community\n            default:\n                throw ApiClientError.noEntityFound\n            }\n        } catch let ApiClientError.response(response, _) where response.couldntFindObject {\n            throw ApiClientError.noEntityFound\n        }\n    }\n    \n    func searchCommunities(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        filter: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) async throws -> [Community2Snapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            try LemmySearchRequest(\n                endpoint: endpoint,\n                q: query,\n                communityId: nil,\n                communityName: nil,\n                creatorId: nil,\n                type_: .communities,\n                sort: sort.apiType(for: endpoint),\n                listingType: filter.apiType,\n                page: page,\n                limit: limit,\n                postTitleOnly: false,\n                searchTerm: query,\n                searchTitleOnly: false\n            )\n        }\n        return try response.communities.map { try .init(from: $0) } \n    }\n\n    func editCommunityDescription(id: Int, newValue: String?) async throws -> Community2Snapshot {\n         let response = try await performingForEndpoint { endpoint in\n            LemmyEditCommunityRequest(\n                endpoint: endpoint,\n                communityId: id,\n                title: nil,\n                // In the v4 API, the `description` field is for the short description\n                description: endpoint == .v3 ? newValue : nil,\n                icon: nil,\n                banner: nil,\n                nsfw: nil,\n                postingRestrictedToMods: nil,\n                discussionLanguages: nil,\n                visibility: nil,\n                sidebar: newValue,\n                summary: nil\n            )\n        }\n        return try .init(from: response.communityView)\n    }\n    \n    @discardableResult\n    func getSubscriptionList(page: Int, limit: Int) async throws -> [Community2Snapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyListCommunitiesRequest(\n                endpoint: endpoint,\n                type_: .subscribed,\n                sort: endpoint == .v4 ? .new(.nameAsc) : .old(.new),\n                showNsfw: true,\n                page: page,\n                limit: limit,\n                timeRangeSeconds: nil,\n                multiCommunityId: nil,\n                searchTerm: nil,\n                searchTitleOnly: nil,\n                pageCursor: nil\n            )\n        }\n        return try response.items.map { try .init(from: $0) }\n    }\n    \n    @discardableResult\n    func subscribeToCommunity(id: Int, subscribe: Bool) async throws -> Community2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyFollowCommunityRequest(endpoint: endpoint, communityId: id, follow: subscribe)\n        }\n        return try .init(from: response.communityView)\n    }\n    \n    @discardableResult\n    func blockCommunity(id: Int, block: Bool) async throws -> Community2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyUserBlockCommunityRequest(endpoint: endpoint, communityId: id, block: block)\n        }\n        switch response {\n        case let .lemmyBlockCommunityResponse(response):\n            return try .init(from: response.communityView)\n        case let .lemmyCommunityResponse(response):\n            return try .init(from: response.communityView)\n        }\n    }\n    \n    @discardableResult\n    func removeCommunity(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Community2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyRemoveCommunityRequest(endpoint: endpoint, communityId: id, removed: remove, reason: reason)\n        }\n        return try .init(from: response.communityView)\n    }\n    \n    func purgeCommunity(id: Int, reason: String?) async throws {\n        _ = try await performingForEndpoint { endpoint in\n            LemmyPurgeCommunityRequest(endpoint: endpoint, communityId: id, reason: reason)\n        }\n    }\n    \n    @discardableResult\n    func addModerator(\n        communityId: Int,\n        personId: Int,\n        added: Bool\n    ) async throws -> (moderators: [Person1Snapshot], community: Community1Snapshot) {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyAddModToCommunityRequest(\n                endpoint: endpoint,\n                communityId: communityId,\n                personId: personId,\n                added: added\n            )\n        }\n        let moderators: [Person1Snapshot] = try response.moderators.map { try .init(from: $0.moderator) }\n        \n        guard let first = response.moderators.first else {\n            throw ApiClientError.unsuccessful\n        }\n        let community: Community1Snapshot = try .init(from: first.community)\n        return (\n            moderators: moderators,\n            community: community\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+Context.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-08-10.\n//\n\nimport Foundation\n\nextension LemmyConnection {\n    // For use inside LemmyConnection only\n    func getRawContext() async throws -> RawContext {\n        // Inconveniently, PieFed offers the `api/v3/site` endpoint in an attempt to look like a Lemmy instance.\n        // We need to check that this *isn't* a PieFed instance, which we can do by making a second request.\n        // The type of request doesn't matter - we're using `UnreadCountRequest` here.\n        \n        let response = try await processingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                async let site = await self.perform(LemmyGetSiteRequest(endpoint: .v3), endpoint: .v3)\n                async let other = await self.perform(LemmyUnreadCountRequest(), endpoint: .v3)\n                do {\n                    _ = try await other\n                } catch ApiClientError.notLoggedIn {\n                    // no-op\n                }\n                let response = try await site\n                return RawContext(site: response, myUser: response.myUser)\n            case .v4:\n                async let site = await self.perform(LemmyGetSiteRequest(endpoint: .v4), endpoint: .v4)\n                \n                var myUser: LemmyMyUserInfo?\n                if self.token != nil {\n                    myUser = try await self.perform(LemmyGetMyUserRequest(), endpoint: .v4)\n                }\n                \n                return try await .init(site: site, myUser: myUser)\n            }\n        }\n        return response\n    }\n    \n    // Calls getRawContext, but if there's already a task running in the `contextDataManager` uses that instead.\n    func getRawContextWithCaching() async throws -> RawContext {\n        if let ongoingTask = contextDataManager.ongoingTask {\n            return try await ongoingTask.result.get()\n        } else {\n            let task = Task(operation: getRawContext)\n            Task.detached {\n                _ = try await self.contextDataManager.getValue(task: task)\n            }\n            return try await task.result.get()\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+Feature.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic extension LemmyConnection {\n    func supports(_ feature: Feature) async throws -> Bool {\n        try await Self.supports(feature, version: version)\n    }\n    \n    func supports(_ feature: Feature, defaultValue: Bool) -> Bool {\n        if let fetchedVersion {\n            return Self.supports(feature, version: fetchedVersion)\n        } else {\n            return defaultValue\n        }\n    }\n\n    static func supports(\n        _ feature: Feature,\n        version: SiteVersion\n    ) -> Bool {\n        switch feature {\n        case let .postSortType(sort):\n            version >= sort.minimumVersion\n        case let .commentSortType(sort):\n            version >= sort.minimumVersion\n        case let .searchSortType(sort):\n            version >= sort.minimumVersion\n        case let .sortTimeRange(timeRange):\n            version >= timeRange.minimumVersion\n        case let .listingType(listingType):\n            version >= listingType.minimumVersion\n        case .searchLocalCommunities, .viewInstanceSettings, .viewInstanceCreationDate, .modlog,\n             .logIn, .signUp, .viewCommunityActiveUsers, .uploadImages,\n             .editAccountSettings, .viewMentionsAndPrivateMessages, .viewReports, .editAndDeletePrivateMessages,\n             .reportPrivateMessages, .viewVotes, .purgeContent, .removeCommunity, .banFromInstance,\n             .banFromCommunity, .editModeratorList, .commentSearch, .undeletePrivateMessages, .searchLocalPeople,\n             .hidePosts, .editDisplayName, .editProfile, .autoMarkPostReadOnInteract, .blockInstances,\n             .fetchLinkMetadata, .unbanWithReason, .customPostThumbnail, .banFromNonLocalCommunity, .editCommunityDescription,\n             .searchLocalComments, .viewInstanceBlockList:\n            true\n        case .moderatorSetNsfw, .userNotes:\n            false\n        }\n    }\n}\n\nprivate extension SiteVersion {\n    static let v0_19_0: Self = .init(\"0.19.0\")\n    static let v0_19_1: Self = .init(\"0.19.1\")\n    static let v0_19_2: Self = .init(\"0.19.2\")\n    static let v0_19_3: Self = .init(\"0.19.3\")\n    static let v0_19_4: Self = .init(\"0.19.4\")\n    static let v0_19_5: Self = .init(\"0.19.5\")\n    static let v0_19_6: Self = .init(\"0.19.6\")\n    static let v0_19_7: Self = .init(\"0.19.7\")\n    static let v0_19_8: Self = .init(\"0.19.8\")\n    static let v0_19_9: Self = .init(\"0.19.9\")\n    static let v0_19_10: Self = .init(\"0.19.10\")\n    static let v0_19_11: Self = .init(\"0.19.11\")\n    static let v0_19_12: Self = .init(\"0.19.12\")\n    static let v1_0_0: Self = .init(\"1.0.0\")\n}\n\nprivate extension PostSortType {\n    var minimumVersion: SiteVersion {\n        switch self {\n        case let .top(timeRange): timeRange.minimumVersion\n        default: .zero\n        }\n    }\n}\n\nprivate extension CommentSortType {\n    var minimumVersion: SiteVersion {\n        switch self {\n        case let .top(timeRange): timeRange == .allTime ? .zero : .v1_0_0\n        default: .zero\n        }\n    }\n}\n\nprivate extension SearchSortType {\n    var minimumVersion: SiteVersion {\n        switch self {\n        case let .top(timeRange): timeRange.minimumVersion\n        default: .zero\n        }\n    }\n}\n\nprivate extension SortTimeRange {\n    var minimumVersion: SiteVersion {\n        switch self {\n        case .allTime: .zero\n        case let .limited(timeInterval): LegacySortTimeRangeLimit(timeInterval)?.minimumVersion ?? .v1_0_0\n        }\n    }\n}\n\nprivate extension LegacySortTimeRangeLimit {\n    var minimumVersion: SiteVersion { .zero }\n}\n\nprivate extension ListingType {\n    var minimumVersion: SiteVersion {\n        switch self {\n        case .suggested: .v1_0_0\n        case .popular: .infinity\n        default: .zero\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+General.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-07.\n//\n\nimport Foundation\n\npublic extension LemmyConnection {\n    func getAccountToken(\n        usernameOrEmail: String,\n        password: String,\n        totpToken: String?\n    ) async throws -> String {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyLoginRequest(\n                endpoint: endpoint,\n                usernameOrEmail: usernameOrEmail,\n                password: password,\n                totp2faToken: totpToken,\n                stayLoggedIn: true\n            )\n        }\n        \n        // I actually don't think this is necessary - the login endpoint seems to throw these errors itself.\n        // I suspect that `registrationCreated` and `verifyEmailSent` can only be true for the `LemmyLoginResponse`\n        // that is returned when signing in. Nevertheless, I've included this just in case.\n        if response.registrationCreated {\n            throw ApiClientError.response(.init(error: \"registration_application_is_pending\"), 200)\n        }\n        if response.verifyEmailSent {\n            throw ApiClientError.response(.init(error: \"email_not_verified\"), 200)\n        }\n        \n        guard let jwt = response.jwt else {\n            assertionFailure()\n            throw ApiClientError.responseMissingRequiredData(\"getAccountToken jwt\")\n        }\n        return jwt\n    }\n    \n    func getUsernameFromToken(token: String) async throws -> String {\n        let username = try await processingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                let request = LemmyGetSiteRequest(endpoint: endpoint)\n                let response = try await self.perform(request, tokenOverride: token, endpoint: .v3)\n                return response.myUser?.localUserView.person.name\n            case .v4:\n                let request = LemmyGetMyUserRequest()\n                let response = try await self.perform(request, tokenOverride: token, endpoint: .v4)\n                return response.localUserView.person.name\n            }\n        }\n        if let username {\n            return username\n        }\n        throw ApiClientError.notLoggedIn\n    }\n    \n    func signUp(\n        username: String,\n        password: String,\n        confirmPassword: String,\n        showNsfw: Bool,\n        email: String?,\n        captcha: Captcha?,\n        captchaAnswer: String?,\n        applicationQuestionResponse: String?\n    ) async throws -> SignUpResponse {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyRegisterRequest(\n                endpoint: endpoint,\n                username: username,\n                password: password,\n                passwordVerify: confirmPassword,\n                showNsfw: showNsfw,\n                email: email,\n                captchaUuid: captcha?.id.uuidString,\n                captchaAnswer: captchaAnswer,\n                honeypot: nil,\n                answer: applicationQuestionResponse,\n                stayLoggedIn: true\n            )\n        }\n        return .init(from: response)\n    }\n    \n    @discardableResult\n    func changePassword(\n        newPassword: String,\n        confirmNewPassword: String,\n        oldPassword: String\n    ) async throws -> String {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyChangePasswordRequest(\n                endpoint: endpoint,\n                newPassword: newPassword,\n                newPasswordVerify: confirmNewPassword,\n                oldPassword: oldPassword,\n                stayLoggedIn: true\n            )\n        }\n        guard let token = response.jwt else {\n            assertionFailure()\n            throw ApiClientError.responseMissingRequiredData(\"changePassword jwt\")\n        }\n        return token\n    }\n    \n    func getCaptcha() async throws -> Captcha {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyGetCaptchaRequest(endpoint: endpoint)\n        }\n        guard let info = response.ok,\n              let uuid = UUID(uuidString: info.uuid),\n              let data = Data(base64Encoded: info.png)\n        else { throw ApiClientError.unsuccessful }\n        \n        return .init(id: uuid, imageData: data)\n    }\n    \n    func resolve(url: URL) async throws -> ResolvedContent {\n        do {\n            // Fix for https://github.com/mlemgroup/mlem/issues/2341\n            let components = url.pathComponents\n            if url.host == baseUrl.host(), components.count > 2 {\n                switch components[1] {\n                case \"c\":\n                    let response = try await performingForEndpoint { endpoint in\n                        LemmyGetCommunityRequest(endpoint: endpoint, id: nil, name: components[2])\n                    }\n                    return try .community(.init(from: response.communityView))\n                case \"u\":\n                    let response = try await performingForEndpoint { endpoint in\n                        LemmyReadPersonRequest(\n                            endpoint: endpoint,\n                            personId: nil,\n                            username: components[2],\n                            sort: nil,\n                            page: 1,\n                            limit: 1,\n                            communityId: nil,\n                            savedOnly: nil\n                        )\n                    }\n                    return try .person(.init(from: response.personView))\n                default:\n                    break\n                }\n            }\n            \n            let response = try await performingForEndpoint { endpoint in\n                LemmyResolveObjectRequest(endpoint: endpoint, q: url.absoluteString)\n            }\n            return try .init(from: response)\n        } catch let ApiClientError.response(response, _) where response.couldntFindObject {\n            throw ApiClientError.noEntityFound\n        }\n    }\n    \n    func getBlocked() async throws -> (people: [Person1Snapshot], communities: [Community1Snapshot], instances: [Instance1Snapshot]) {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyGetSiteRequest(endpoint: endpoint)\n        }\n        \n        guard let myUser = response.myUser else { return ([], [], []) }\n        \n        return try (\n            people: myUser.personBlocks.map { try .init(from: $0.person) },\n            communities: myUser.communityBlocks.map { try .init(from: $0.community) },\n            instances: myUser.instanceBlocks?.compactMap(\\.site).map { try .init(from: $0) } ?? [] // TODO: Lemmy 1.0\n        )\n    }\n    \n    func getModlog(\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        moderatorId: Int? = nil,\n        subjectPersonId: Int? = nil,\n        postId: Int? = nil,\n        commentId: Int? = nil,\n        type: ModlogEntryType? = nil\n    ) async throws -> [ModlogEntrySnapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyGetModLogRequest(\n                endpoint: endpoint,\n                modPersonId: moderatorId,\n                communityId: communityId,\n                page: page,\n                limit: limit,\n                type_: type?.apiType,\n                otherPersonId: subjectPersonId,\n                postId: postId,\n                commentId: commentId,\n                listingType: .all,\n                showBulk: nil,\n                bulkActionParentId: nil,\n                pageCursor: nil\n            )\n        }\n        switch response {\n        case let .lemmyGetModlogResponse(response):\n            return try response.toSnapshots()\n        case let .lemmyPagedResponse(response):\n            return try response.items.compactMap { try .init(from: $0) }\n        }\n    }\n    \n    func getPostLink(url: URL) async throws -> PostLink {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyGetLinkMetadataRequest(endpoint: endpoint, url: url)\n        }\n        return .init(\n            content: url,\n            thumbnail: response.metadata.image,\n            label: response.metadata.title ?? url.absoluteString\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+Image.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-05.\n//\n\nimport Foundation\nimport Rest\n\npublic extension LemmyConnection {\n    func uploadImage(\n        _ imageData: Data,\n        fileExtension: String,\n        onProgress progressCallback: @escaping (_ progress: Double) -> Void = { _ in }\n    ) async throws -> ImageUpload1Snapshot {\n        guard let token else { throw ApiClientError.notLoggedIn }\n        var request = mlemUrlRequest(url: baseUrl.appending(path: \"pictrs/image\"))\n        request.httpMethod = \"POST\"\n        \n        let boundary = UUID().uuidString\n        request.setValue(\"multipart/form-data; boundary=\\(boundary)\", forHTTPHeaderField: \"Content-Type\")\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n        \n        let encodedData = createMultiPartForm(\n            boundary: boundary,\n            contentType: \"image/png\",\n            name: \"images[]\",\n            fileName: \"image.\\(fileExtension)\",\n            imageData: imageData,\n            auth: token\n        )\n        \n        let (data, _) = try await restClient.urlSession.upload(\n            for: request,\n            from: encodedData,\n            delegate: ImageUploadDelegate(callback: progressCallback)\n        )\n        \n        do {\n            let response = try JSONDecoder.defaultDecoder.decode(LemmyPictrsUploadResponse.self, from: data)\n            guard let file = response.files?.first else { throw ApiClientError.noEntityFound }\n            return .init(from: file, baseUrl: baseUrl)\n        } catch DecodingError.dataCorrupted {\n            let text = String(decoding: data, as: UTF8.self)\n            if text.contains(\"413 Request Entity Too Large\") {\n                throw ApiClientError.imageTooLarge\n            }\n            throw ApiClientError.decoding(data, nil)\n        }\n    }\n    \n    func deleteImage(alias: String, deleteToken: String) async throws {\n        guard let token else { throw ApiClientError.notLoggedIn }\n        var request = mlemUrlRequest(url: baseUrl.appending(path: \"pictrs/image/delete/\\(deleteToken)/\\(alias)\"))\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n        let response = try await restClient.execute(request)\n        if let response = response.1 as? HTTPURLResponse {\n            if response.statusCode != 204 {\n                throw ApiClientError.response(.init(error: \"Unexpected status code\"), response.statusCode)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+Inbox.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-08.\n//\n\nimport Foundation\n\npublic extension LemmyConnection {\n    func getMessages(\n        creatorId: Int? = nil,\n        page: Int,\n        limit: Int,\n        unreadOnly: Bool = false\n    ) async throws -> [Message2Snapshot] {\n        let response = try await performingForEndpoint { _ in\n            LemmyGetPrivateMessageRequest(\n                unreadOnly: unreadOnly,\n                page: page,\n                limit: limit,\n                creatorId: creatorId\n            )\n        }\n        return try response.privateMessages.map { try .init(from: $0) }\n    }\n    \n    func getReplyNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?) {\n        try await processingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                guard let page else { throw ApiClientError.featureUnsupported }\n                let request = LemmyListRepliesRequest(\n                    sort: .new,\n                    page: page,\n                    limit: limit,\n                    unreadOnly: unreadOnly\n                )\n                let response = try await self.perform(request, endpoint: .v3)\n                return try (notifications: response.replies.map { try .init(from: $0) }, cursor: nil)\n            case .v4:\n                let request = LemmyListNotificationsRequest(\n                    type_: .reply,\n                    unreadOnly: unreadOnly,\n                    creatorId: nil,\n                    pageCursor: cursor,\n                    limit: limit\n                )\n                let response = try await self.perform(request, endpoint: .v4)\n                return try (\n                    notifications: response.items.map { try .init(from: $0) },\n                    cursor: response.nextPage\n                )\n            }\n        }\n    }\n\n    func getMentionNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?) {\n        try await processingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                guard let page else { throw ApiClientError.featureUnsupported }\n                let request = LemmyListMentionsRequest(\n                    sort: .new,\n                    page: page,\n                    limit: limit,\n                    unreadOnly: unreadOnly\n                )\n                let response = try await self.perform(request, endpoint: .v3)\n                return try (notifications: response.mentions.map { try .init(from: $0) }, cursor: nil)\n            case .v4:\n                let request = LemmyListNotificationsRequest(\n                    type_: .mention,\n                    unreadOnly: unreadOnly,\n                    creatorId: nil,\n                    pageCursor: cursor,\n                    limit: limit\n                )\n                let response = try await self.perform(request, endpoint: .v4)\n                return try (\n                    notifications: response.items.map { try .init(from: $0) },\n                    cursor: response.nextPage\n                )\n            }\n        }\n    }\n\n    func getMessageNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?) {\n        try await processingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                guard let page else { throw ApiClientError.featureUnsupported }\n                let request = LemmyGetPrivateMessageRequest(\n                    unreadOnly: unreadOnly,\n                    page: page,\n                    limit: limit,\n                    creatorId: nil\n                )\n                let response = try await self.perform(request, endpoint: .v3)\n                return try (notifications: response.privateMessages.map { try .init(from: $0) }, cursor: nil)\n            case .v4:\n                let request = LemmyListNotificationsRequest(\n                    type_: .privateMessage,\n                    unreadOnly: unreadOnly,\n                    creatorId: nil,\n                    pageCursor: cursor,\n                    limit: limit\n                )\n                let response = try await self.perform(request, endpoint: .v4)\n                return try (\n                    notifications: response.items.map { try .init(from: $0) },\n                    cursor: response.nextPage\n                )\n            }\n        }\n    }\n    \n    func markNotificationAsRead(\n        type: InboxNotificationContentType,\n        id: Int,\n        contentId: Int,\n        read: Bool = true\n    ) async throws {\n        try await processingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                try await self.markNotificationAsReadV3(type: type, contentId: contentId, read: read)\n            case .v4:\n                let request = LemmyMarkNotificationAsReadRequest(notificationId: id, read: read)\n                try await self.perform(request, endpoint: .v4)\n            }\n        }\n    }\n\n    private func markNotificationAsReadV3(\n        type: InboxNotificationContentType,\n        contentId: Int,\n        read: Bool\n    ) async throws {\n        switch type {\n        case .reply:\n            try await self.perform(LemmyMarkReplyAsReadRequest(commentReplyId: contentId, read: read), endpoint: .v3)\n        case .mention:\n            try await self.perform(LemmyMarkPersonMentionAsReadRequest(personMentionId: contentId, read: read), endpoint: .v3)\n        case .message:\n            try await self.perform(LemmyMarkPmAsReadRequest(privateMessageId: contentId, read: read), endpoint: .v3)\n        }\n    }\n    \n    func markAllAsRead() async throws {\n        _ = try await performingForEndpoint { endpoint in\n            LemmyMarkAllNotificationsReadRequest(endpoint: endpoint)\n        }\n    }\n    \n    func getPersonalUnreadCount() async throws -> PersonalUnreadCountSnapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyUnreadCountRequest()\n        }\n        return try .init(from: response)\n    }\n    \n    func createMessage(personId: Int, content: String) async throws -> Message2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyCreatePrivateMessageRequest(\n                endpoint: endpoint,\n                content: content,\n                recipientId: personId\n            )\n        }\n        return try .init(from: response.privateMessageView)\n    }\n    \n    @discardableResult\n    func editMessage(id: Int, content: String) async throws -> Message2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyEditPrivateMessageRequest(\n                endpoint: endpoint,\n                privateMessageId: id,\n                content: content\n            )\n        }\n        return try .init(from: response.privateMessageView)\n    }\n    \n    @discardableResult\n    func reportMessage(id: Int, reason: String) async throws -> ReportSnapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyCreatePmReportRequest(endpoint: endpoint, privateMessageId: id, reason: reason)\n        }\n        return try .init(from: response.privateMessageReportView)\n    }\n    \n    @discardableResult\n    func deleteMessage(id: Int, delete: Bool) async throws -> Message2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyDeletePrivateMessageRequest(endpoint: endpoint, privateMessageId: id, deleted: delete)\n        }\n        return try .init(from: response.privateMessageView)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+Instance.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-08.\n//\n\nimport Foundation\n\npublic extension LemmyConnection {\n    func getMyInstance() async throws -> Instance3Snapshot {\n        let rawContext = try await getRawContextWithCaching()\n        return try .init(from: rawContext.site)\n    }\n    \n    func getFederatedInstances() async throws -> FederationPolicy {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyGetFederatedInstancesRequest(endpoint: endpoint)\n        }\n        switch response {\n        case let .lemmyLegacyGetFederatedInstancesResponse(response):\n            if let federatedInstances = response.federatedInstances {\n                return .init(from: federatedInstances)\n            }\n            throw ApiClientError.noEntityFound\n        case let .lemmyPagedResponse(response):\n            return .init(from: response.items)\n        }\n    }\n    \n    func blockInstance(instanceId: Int, block: Bool) async throws {\n        _ = try await performingForEndpoint { endpoint in\n            LemmyUserBlockInstanceCommunitiesRequest(endpoint: endpoint, instanceId: instanceId, block: block)\n        }\n    }\n    \n    @discardableResult\n    func addAdmin(personId: Int, added: Bool) async throws -> [Person2Snapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyAddAdminRequest(endpoint: endpoint, personId: personId, added: added)\n        }\n        return try response.admins.map { try .init(from: $0) }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+Person.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-06.\n//\n\nimport Foundation\n\npublic extension LemmyConnection {\n    func getPerson(id: Int) async throws -> Person3Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyReadPersonRequest(\n                endpoint: endpoint,\n                personId: id,\n                username: nil,\n                sort: .new,\n                page: 1,\n                limit: 1,\n                communityId: nil,\n                savedOnly: nil\n            )\n        }\n        return try .init(from: response)\n    }\n    \n    func getPerson(url: URL) async throws -> Person2Snapshot {\n        do {\n            let result = try await resolve(url: url)\n            switch result {\n            case let .person(person):\n                return person\n            default:\n                throw ApiClientError.noEntityFound\n            }\n        } catch let ApiClientError.response(response, _) where response.couldntFindObject {\n            throw ApiClientError.noEntityFound\n        }\n    }\n    \n    func getPerson(username: String) async throws -> Person3Snapshot {\n        do {\n            let response = try await performingForEndpoint { endpoint in\n                LemmyReadPersonRequest(\n                    endpoint: endpoint,\n                    personId: nil,\n                    username: username,\n                    sort: nil,\n                    page: nil,\n                    limit: nil,\n                    communityId: nil,\n                    savedOnly: nil\n                )\n            }\n            return try .init(from: response)\n        } catch let ApiClientError.response(response, _) where response.couldntFindObject {\n            throw ApiClientError.noEntityFound\n        }\n    }\n    \n    /// `filter` can be set to `.local` from 0.19.4 onwards.\n    func searchPeople(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        filter: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) async throws -> [Person2Snapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            try LemmySearchRequest(\n                endpoint: endpoint,\n                q: query,\n                communityId: nil,\n                communityName: nil,\n                creatorId: nil,\n                type_: .users,\n                sort: sort.apiType(for: endpoint),\n                listingType: filter.apiType,\n                page: page,\n                limit: limit,\n                postTitleOnly: false,\n                searchTerm: query,\n                searchTitleOnly: false\n            )\n        }\n        return try response.users?.map { try .init(from: $0) } ?? []\n    }\n    \n    @discardableResult\n    func blockPerson(id: Int, block: Bool) async throws -> Person2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyUserBlockPersonRequest(endpoint: endpoint, personId: id, block: block)\n        }\n        switch response {\n        case let .lemmyBlockPersonResponse(response):\n            return try .init(from: response.personView)\n        case let .lemmyPersonResponse(response):\n            return try .init(from: response.personView)\n        }\n    }\n    \n    @discardableResult\n    func banPersonFromCommunity(\n        personId: Int,\n        communityId: Int,\n        ban: Bool,\n        removeContent: Bool,\n        reason: String?,\n        expires: Date? = nil\n    ) async throws -> Person1Snapshot {\n        let expiryTimestamp: Int?\n        if let expires {\n            expiryTimestamp = Int(expires.timeIntervalSince1970)\n        } else {\n            expiryTimestamp = nil\n        }\n        let response = try await performingForEndpoint { endpoint in\n            LemmyBanFromCommunityRequest(\n                endpoint: endpoint,\n                communityId: communityId,\n                personId: personId,\n                ban: ban,\n                removeData: removeContent,\n                reason: reason,\n                expires: expiryTimestamp,\n                removeOrRestoreData: removeContent,\n                expiresAt: expiryTimestamp\n            )\n        }\n        switch response {\n        case let .lemmyBanFromCommunityResponse(response):\n            guard response.banned == ban else { throw ApiClientError.unsuccessful }\n            return try .init(from: response.personView.person)\n        case let .lemmyPersonResponse(response):\n            return try .init(from: response.personView.person)\n        }\n    }\n    \n    @discardableResult\n    func banPersonFromInstance(\n        personId: Int,\n        ban: Bool,\n        removeContent: Bool,\n        reason: String?,\n        expires: Date? = nil\n    ) async throws -> Person2Snapshot {\n        let expiryTimestamp: Int?\n        if let expires {\n            expiryTimestamp = Int(expires.timeIntervalSince1970)\n        } else {\n            expiryTimestamp = nil\n        }\n        let response = try await performingForEndpoint { endpoint in\n            LemmyBanFromSiteRequest(\n                endpoint: endpoint,\n                personId: personId,\n                ban: ban,\n                removeData: removeContent,\n                reason: reason,\n                expires: expiryTimestamp,\n                removeOrRestoreData: removeContent,\n                expiresAt: expiryTimestamp\n            )\n        }\n        return try .init(from: response.personView)\n    }\n    \n    func purgePerson(id: Int, reason: String?) async throws {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyPurgePersonRequest(endpoint: endpoint, personId: id, reason: reason)\n        }\n        guard response.success else { throw ApiClientError.unsuccessful }\n    }\n    \n    func getContent(\n        authorId id: Int,\n        sort: PostSortType,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool? = nil,\n        communityId: Int? = nil\n    ) async throws -> (person: Person3Snapshot, posts: [Post2Snapshot], comments: [Comment2Snapshot]) {\n        let response = try await performingForEndpoint { endpoint in\n            if endpoint == .v4 {\n                // TODO: Use LemmyListPersonContentRequest here\n                throw ApiClientError.featureUnsupported\n            }\n            return LemmyReadPersonRequest(\n                endpoint: endpoint,\n                personId: id,\n                username: nil,\n                sort: sort.v3ApiType,\n                page: page,\n                limit: limit,\n                communityId: nil,\n                savedOnly: savedOnly\n            )\n        }\n        return try (\n            person: .init(from: response),\n            posts: response.posts?.map { try .init(from: $0) } ?? [],\n            comments: response.comments?.map { try .init(from: $0) } ?? []\n        )\n    }\n    \n    func getMyPerson() async throws -> (person: Person4Snapshot?, instance: Instance3Snapshot, blocks: BlockListSnapshot?) {\n        let rawContext = try await getRawContextWithCaching()\n        var person: Person4Snapshot?\n        var blocks: BlockListSnapshot?\n        if let myUser = rawContext.myUser {\n            person = try .init(from: myUser)\n            blocks = try .init(from: myUser)\n        }\n        \n        return try (\n            person: person,\n            instance: .init(from: rawContext.site),\n            blocks: blocks\n        )\n    }\n    \n    func deleteAccount(password: String, deleteContent: Bool) async throws {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyDeleteAccountRequest(\n                endpoint: endpoint,\n                password: password,\n                deleteContent: deleteContent\n            )\n        }\n        guard response.success else {\n            throw ApiClientError.unsuccessful\n        }\n    }\n\n    func editNote(id: Int, content: String?) async throws {\n        throw ApiClientError.featureUnsupported\n    }\n\n    func editProfile(details: ProfileDetails) async throws {\n        let response = try await performingForEndpoint { endpoint in\n            LemmySaveUserSettingsRequest(\n                endpoint: endpoint,\n                showNsfw: nil,\n                blurNsfw: nil,\n                autoExpand: nil,\n                showScores: nil,\n                theme: nil,\n                defaultSortType: nil,\n                defaultListingType: nil,\n                interfaceLanguage: nil,\n                avatar: details.avatar?.absoluteString ?? \"\",\n                banner: details.banner?.absoluteString ?? \"\",\n                displayName: details.displayName,\n                email: nil,\n                bio: details.description,\n                matrixUserId: details.matrixUserId,\n                showAvatars: nil,\n                sendNotificationsToEmail: nil,\n                botAccount: nil,\n                showBotAccounts: nil,\n                showReadPosts: nil,\n                discussionLanguages: nil,\n                openLinksInNewTab: nil,\n                infiniteScrollEnabled: nil,\n                postListingMode: nil,\n                enableKeyboardNavigation: nil,\n                enableAnimatedImages: nil,\n                collapseBotComments: nil,\n                showUpvotes: nil,\n                showDownvotes: nil,\n                showUpvotePercentage: nil,\n                defaultPostSortType: nil,\n                defaultPostTimeRangeSeconds: nil,\n                defaultItemsPerPage: nil,\n                defaultCommentSortType: nil,\n                blockingKeywords: nil,\n                animatedImagesEnabled: nil,\n                privateMessagesEnabled: nil,\n                showScore: nil,\n                autoMarkFetchedPostsAsRead: nil,\n                hideMedia: nil,\n                showPersonVotes: nil\n            )\n        }\n        guard response.success else {\n            throw ApiClientError.unsuccessful\n        }\n    }\n    \n    func editAccountSettings(\n        showNsfw: Bool?,\n        showScores: Bool?,\n        theme: String?,\n        defaultListingType: ListingType?,\n        interfaceLanguage: String?,\n        avatar: String?,\n        banner: String?,\n        displayName: String?,\n        email: String?,\n        bio: String?,\n        matrixUserId: String?,\n        showAvatars: Bool?,\n        sendNotificationsToEmail: Bool?,\n        botAccount: Bool?,\n        showBotAccounts: Bool?,\n        showReadPosts: Bool?,\n        discussionLanguages: [Int]?,\n        openLinksInNewTab: Bool?,\n        blurNsfw: Bool?,\n        autoExpand: Bool?,\n        infiniteScrollEnabled: Bool?,\n        postListingMode: PostFeedViewMode?,\n        enableKeyboardNavigation: Bool?,\n        enableAnimatedImages: Bool?,\n        collapseBotComments: Bool?,\n        showUpvotes: Bool?,\n        showDownvotes: Bool?,\n        showUpvotePercentage: Bool?\n    ) async throws {\n        let response = try await performingForEndpoint { endpoint in\n            LemmySaveUserSettingsRequest(\n                endpoint: endpoint,\n                showNsfw: showNsfw,\n                blurNsfw: blurNsfw,\n                autoExpand: autoExpand,\n                showScores: showScores,\n                theme: theme,\n                defaultSortType: nil,\n                defaultListingType: defaultListingType?.apiType,\n                interfaceLanguage: interfaceLanguage,\n                avatar: avatar,\n                banner: banner,\n                displayName: displayName,\n                email: email,\n                bio: bio,\n                matrixUserId: matrixUserId,\n                showAvatars: showAvatars,\n                sendNotificationsToEmail: sendNotificationsToEmail,\n                botAccount: botAccount,\n                showBotAccounts: showBotAccounts,\n                showReadPosts: showReadPosts,\n                discussionLanguages: discussionLanguages,\n                openLinksInNewTab: openLinksInNewTab,\n                infiniteScrollEnabled: infiniteScrollEnabled,\n                postListingMode: postListingMode?.apiType,\n                enableKeyboardNavigation: enableKeyboardNavigation,\n                enableAnimatedImages: enableAnimatedImages,\n                collapseBotComments: collapseBotComments,\n                showUpvotes: showUpvotes,\n                showDownvotes: showDownvotes.map { .init(showVotes: $0) },\n                showUpvotePercentage: showUpvotePercentage,\n                defaultPostSortType: nil,\n                defaultPostTimeRangeSeconds: nil,\n                defaultItemsPerPage: nil,\n                defaultCommentSortType: nil,\n                blockingKeywords: nil,\n                animatedImagesEnabled: nil,\n                privateMessagesEnabled: nil,\n                showScore: nil,\n                autoMarkFetchedPostsAsRead: nil,\n                hideMedia: nil,\n                showPersonVotes: nil\n            )\n        }\n        guard response.success else {\n            throw ApiClientError.unsuccessful\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+Post.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-05.\n//\n\nimport Foundation\n\npublic extension LemmyConnection {\n    func getPosts(\n        communityId: Int,\n        sort: PostSortType,\n        page: Int,\n        cursor: String?,\n        limit: Int,\n        filter: GetContentFilter? = nil,\n        showHidden: Bool = false\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?) {\n        let response = try await performingForEndpoint { endpoint in\n            try LemmyListPostsRequest(\n                endpoint: endpoint,\n                type_: .all,\n                sort: sort.apiType(for: endpoint),\n                page: cursor == nil ? page : nil,\n                limit: limit,\n                communityId: communityId,\n                communityName: nil,\n                savedOnly: filter == .saved,\n                likedOnly: filter == .upvoted,\n                dislikedOnly: filter == .downvoted,\n                pageCursor: cursor,\n                showHidden: showHidden,\n                showRead: nil,\n                showNsfw: nil,\n                timeRangeSeconds: sort.timeRangeSeconds,\n                multiCommunityId: nil,\n                multiCommunityName: nil,\n                hideMedia: nil,\n                markAsRead: nil,\n                noCommentsOnly: nil,\n                searchTerm: nil,\n                searchTitleOnly: nil,\n                searchUrlOnly: nil\n            )\n        }\n        return try (\n            posts: response.items.map { try .init(from: $0) },\n            cursor: response.nextPage\n        )\n    }\n    \n    func getPosts(\n        feed: ListingType,\n        sort: PostSortType,\n        page: Int,\n        cursor: String?,\n        limit: Int,\n        filter: GetContentFilter? = nil,\n        showHidden: Bool = false\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?) {\n        let response = try await performingForEndpoint { endpoint in\n            try LemmyListPostsRequest(\n                endpoint: endpoint,\n                type_: feed.apiType,\n                sort: sort.apiType(for: endpoint),\n                page: cursor == nil ? page : nil,\n                limit: limit,\n                communityId: nil,\n                communityName: nil,\n                savedOnly: filter == .saved,\n                likedOnly: filter == .upvoted,\n                dislikedOnly: filter == .downvoted,\n                pageCursor: cursor,\n                showHidden: showHidden,\n                showRead: nil,\n                showNsfw: nil,\n                timeRangeSeconds: sort.timeRangeSeconds,\n                multiCommunityId: nil,\n                multiCommunityName: nil,\n                hideMedia: nil,\n                markAsRead: nil,\n                noCommentsOnly: nil,\n                searchTerm: nil,\n                searchTitleOnly: nil,\n                searchUrlOnly: nil\n            )\n        }\n        return try (\n            posts: response.items.map { try .init(from: $0) },\n            cursor: response.nextPage\n        )\n    }\n\n    func getPosts(\n        personId: Int,\n        communityId: Int? = nil,\n        sort: PostSortType = .new,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool = false\n    ) async throws -> (person: Person3Snapshot, posts: [Post2Snapshot]) {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyReadPersonRequest(\n                endpoint: endpoint,\n                personId: personId,\n                username: nil,\n                sort: sort.v3ApiType,\n                page: page,\n                limit: limit,\n                communityId: communityId,\n                savedOnly: savedOnly\n            )\n        }\n        return try (\n            person: .init(from: response),\n            posts: response.posts?.map { try .init(from: $0) } ?? []\n        )\n    }\n\n    func getPostHistory(\n        type: GetContentFilter,\n        page: Int?,\n        cursor: String?,\n        limit: Int\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?) {\n        try await processingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                // Cursors are supported on v3, but are super slow when\n                // querying saved posts. For that reason, we're considering them\n                // unsupported and requiring a page number instead.\n                // See LemmyNet/lemmy#6171\n\n                guard let page else {\n                    throw ApiClientError.featureUnsupported\n                }\n\n                let request = LemmyListPostsRequest(\n                    endpoint: .v3,\n                    type_: .all,\n                    sort: .old(.new),\n                    page: page,\n                    limit: limit,\n                    communityId: nil,\n                    communityName: nil,\n                    savedOnly: type == .saved,\n                    likedOnly: type == .upvoted,\n                    dislikedOnly: type == .downvoted,\n                    pageCursor: nil,\n                    showHidden: false,\n                    showRead: nil,\n                    showNsfw: nil,\n                    timeRangeSeconds: nil,\n                    multiCommunityId: nil,\n                    multiCommunityName: nil,\n                    hideMedia: nil,\n                    markAsRead: nil,\n                    noCommentsOnly: nil,\n                    searchTerm: nil,\n                    searchTitleOnly: nil,\n                    searchUrlOnly: nil\n                )\n                let response = try await self.perform(request, endpoint: .v3)        \n                return try (\n                    posts: response.items.map { try .init(from: $0) },\n                    // Cursor intentionally omitted here. See Comment above\n                    cursor: nil\n                )\n            case .v4:\n                if let page, page != 1 {\n                    throw ApiClientError.featureUnsupported\n                }\n\n                switch type {\n                case .saved:\n                let request = LemmyListPersonSavedRequest(\n                    type_: .all,\n                    pageCursor: cursor,\n                    limit: limit\n                )\n                let response = try await self.perform(request, endpoint: .v4)\n                return try (\n                    posts: response.items.compactMap(\\.postValue).map { try .init(from: $0) },\n                    cursor: response.nextPage\n                )\n                default: \n                let request = LemmyListPersonLikedRequest(\n                    type_: .all,\n                    likeType: type == .upvoted ? .likedOnly : .dislikedOnly,\n                    pageCursor: cursor,\n                    limit: limit\n                )\n                let response = try await self.perform(request, endpoint: .v4)\n                return try (\n                    posts: response.items.compactMap(\\.postValue).map { try .init(from: $0) },\n                    cursor: response.nextPage\n                )\n                }\n            }\n        }\n    }\n\n    func getPost(id: Int) async throws -> Post3Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyGetPostRequest(\n                endpoint: endpoint,\n                id: id,\n                commentId: nil\n            )\n        }\n        return try .init(from: response)\n    }\n    \n    func getPost(url: URL) async throws -> Post2Snapshot {\n        do {\n            let result = try await resolve(url: url)\n            switch result {\n            case let .post(post):\n                return post\n            default:\n                throw ApiClientError.noEntityFound\n            }\n        } catch let ApiClientError.response(response, _) where response.couldntFindObject {\n            throw ApiClientError.noEntityFound\n        }\n    }\n    \n    // This method should be removed in favor of the below method once we drop support for versions before Lemmy 1.0\n    func searchPosts(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: PostSortType\n    ) async throws -> [Post2Snapshot] {\n        try await searchPosts(\n            query: query,\n            page: page,\n            limit: limit,\n            communityId: communityId,\n            creatorId: creatorId,\n            filter: filter,\n            createSortType: { try sort.apiType(for: $0) },\n            timeRangeSeconds: sort.timeRangeSeconds\n        )\n    }\n    \n    func searchPosts(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: SearchSortType\n    ) async throws -> [Post2Snapshot] {\n        try await searchPosts(\n            query: query,\n            page: page,\n            limit: limit,\n            communityId: communityId,\n            creatorId: creatorId,\n            filter: filter,\n            createSortType: { try sort.apiType(for: $0) },\n            timeRangeSeconds: sort.timeRangeSeconds\n        )\n    }\n    \n    private func searchPosts(\n        query: String,\n        page: Int,\n        limit: Int,\n        communityId: Int?,\n        creatorId: Int?,\n        filter: ListingType,\n        createSortType: @escaping (LemmyEndpointVersion) throws -> LemmySearchSortTypeBridge,\n        timeRangeSeconds: Int?\n    ) async throws -> [Post2Snapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            try LemmySearchRequest(\n                endpoint: endpoint,\n                q: query,\n                communityId: communityId,\n                communityName: nil,\n                creatorId: creatorId,\n                type_: .posts,\n                sort: createSortType(endpoint),\n                listingType: filter.apiType,\n                page: page,\n                limit: limit,\n                postTitleOnly: false,\n                searchTerm: query,\n                searchTitleOnly: false\n            )\n        }\n        return try response.posts.map { try .init(from: $0) }\n    }\n    \n    func markPostsAsRead(ids: Set<Int>, read: Bool) async throws {\n        guard !ids.isEmpty else { return }\n        \n        try await processingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                let request = LemmyMarkPostAsReadRequest(endpoint: .v3, postId: nil, postIds: Array(ids), read: read)\n                try await self.perform(request, endpoint: .v3)\n            case .v4:\n                let request = LemmyMarkPostsAsReadRequest(postIds: Array(ids), read: read)\n                try await self.perform(request, endpoint: .v4)\n            }\n        }\n    }\n    \n    func markPostAsRead(id: Int, read: Bool) async throws {\n        // Could we do something with the response here?\n        _ = try await performingForEndpoint { endpoint in\n            LemmyMarkPostAsReadRequest(\n                endpoint: endpoint,\n                postId: id,\n                postIds: [id],\n                read: read\n            )\n        }\n    }\n    \n    @discardableResult\n    func voteOnPost(id: Int, score: ScoringOperation) async throws -> Post2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyLikePostRequest(\n                endpoint: endpoint,\n                postId: id,\n                score: score.rawValue,\n                isUpvote: score.booleanValue\n            )\n        }\n        return try .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func savePost(id: Int, save: Bool) async throws -> Post2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmySavePostRequest(\n                endpoint: endpoint,\n                postId: id,\n                save: save\n            )\n        }\n        return try .init(from: response.postView, overrideRead: true)\n    }\n    \n    @discardableResult\n    func deletePost(id: Int, delete: Bool) async throws -> Post2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyDeletePostRequest(\n                endpoint: endpoint,\n                postId: id,\n                deleted: delete\n            )\n        }\n        return try .init(from: response.postView)\n    }\n    \n    // Marking many posts as hidden was possible in 0.19.0, but this was removed in 1.0.0\n    func hidePost(id: Int, hide: Bool) async throws {\n        // Could we do something with the response here?\n        _ = try await performingForEndpoint { endpoint in\n            LemmyHidePostRequest(\n                endpoint: endpoint,\n                postIds: [id],\n                hide: hide,\n                postId: id\n            )\n        }\n    }\n    \n    func createPost(\n        communityId: Int,\n        title: String,\n        content: String? = nil,\n        linkUrl: URL? = nil,\n        altText: String? = nil,\n        thumbnail: URL? = nil,\n        nsfw: Bool,\n        languageId: Int? = nil\n    ) async throws -> Post2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyCreatePostRequest(\n                endpoint: endpoint,\n                name: title,\n                communityId: communityId,\n                url: linkUrl,\n                body: content,\n                honeypot: nil,\n                nsfw: nsfw,\n                languageId: languageId,\n                altText: altText,\n                customThumbnail: thumbnail?.absoluteString,\n                tags: nil,\n                scheduledPublishTimeAt: nil\n            )\n        }\n        return try .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func editPost(\n        id: Int,\n        title: String,\n        content: String? = nil,\n        linkUrl: URL? = nil,\n        altText: String? = nil,\n        thumbnail: URL? = nil,\n        nsfw: Bool,\n        languageId: Int? = nil\n    ) async throws -> Post2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyEditPostRequest(\n                endpoint: endpoint,\n                postId: id,\n                name: title,\n                url: linkUrl,\n                body: content,\n                nsfw: nsfw,\n                languageId: languageId,\n                altText: altText,\n                customThumbnail: thumbnail?.absoluteString,\n                scheduledPublishTimeAt: nil,\n                tags: nil\n            )\n        }\n        return try .init(from: response.postView)\n    }\n    \n    func replyToPost(\n        id: Int,\n        content: String,\n        languageId: Int? = nil\n    ) async throws -> Comment2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyCreateCommentRequest(\n                endpoint: endpoint,\n                content: content,\n                postId: id,\n                parentId: nil,\n                languageId: languageId\n            )\n        }\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func reportPost(id: Int, reason: String) async throws -> ReportSnapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyCreatePostReportRequest(\n                endpoint: endpoint,\n                postId: id,\n                reason: reason,\n                violatesInstanceRules: nil\n            )\n        }\n        return try .init(from: response.postReportView)\n    }\n    \n    func purgePost(id: Int, reason: String?) async throws {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyPurgePostRequest(endpoint: endpoint, postId: id, reason: reason)\n        }\n        guard response.success else { throw ApiClientError.unsuccessful }\n    }\n    \n    @discardableResult\n    func removePost(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Post2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyRemovePostRequest(\n                endpoint: endpoint,\n                postId: id,\n                removed: remove,\n                reason: reason,\n                removeChildren: nil\n            )\n        }\n        return try .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func pinPost(\n        id: Int,\n        pin: Bool,\n        to target: PostFeatureType\n    ) async throws -> Post2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyFeaturePostRequest(\n                endpoint: endpoint,\n                postId: id,\n                featured: pin,\n                featureType: target.apiType\n            )\n        }\n        return try .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func lockPost(id: Int, lock: Bool) async throws -> Post2Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyLockPostRequest(\n                endpoint: endpoint,\n                postId: id,\n                locked: lock,\n                reason: nil\n            )\n        }\n        return try .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func setPostNsfw(id: Int, nsfw: Bool) async throws -> Post1Snapshot {\n        let response = try await performingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                throw ApiClientError.featureUnsupported\n            case .v4:\n                return LemmyModEditPostRequest(postId: id, nsfw: nsfw, tags: nil)\n            }\n        }\n        return try .init(from: response.postView.post)\n    }\n\n    @discardableResult\n    func getPostVotes(\n        id: Int,\n        page: Int = 1,\n        limit: Int = 20\n    ) async throws -> [PersonVoteSnapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyListPostLikesRequest(\n                endpoint: endpoint,\n                postId: id,\n                page: page,\n                limit: limit,\n                pageCursor: nil\n            )\n        }\n        return try response.items.map { try .init(from: $0) }\n    }\n\n    @discardableResult\n    func voteInPoll(postId: Int, choiceIds: Set<Int>) async throws -> Post2Snapshot {\n        throw ApiClientError.featureUnsupported\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+RegistrationApplication.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-08.\n//\n\nimport Foundation\n\npublic extension LemmyConnection {\n    func getRegistrationApplicationCount() async throws -> Int {\n        let response = try await performingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n            LemmyGetUnreadRegistrationApplicationCountRequest()\n            case .v4:\n                throw ApiClientError.featureUnsupported\n            }\n        }\n        return response.registrationApplications\n    }\n    \n    func getRegistrationApplications(\n        page: Int = 1,\n        limit: Int = 20,\n        unreadOnly: Bool = false\n    ) async throws -> [RegistrationApplicationSnapshot] {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyListRegistrationApplicationsRequest(\n                endpoint: endpoint,\n                unreadOnly: unreadOnly,\n                page: page,\n                limit: limit,\n                pageCursor: nil\n            )\n        }\n        return try response.items.map { try .init(from: $0) }\n    }\n    \n    @discardableResult\n    func approveRegistrationApplication(id: Int) async throws -> RegistrationApplicationSnapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyApproveRegistrationApplicationRequest(\n                endpoint: endpoint,\n                id: id,\n                approve: true,\n                denyReason: nil\n            )\n        }\n        return try .init(from: response.registrationApplication)\n    }\n    \n    @discardableResult\n    func denyRegistrationApplication(id: Int, reason: String?) async throws -> RegistrationApplicationSnapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyApproveRegistrationApplicationRequest(\n                endpoint: endpoint,\n                id: id,\n                approve: false,\n                denyReason: reason\n            )\n        }\n        return try .init(from: response.registrationApplication)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection+Report.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-08.\n//\n\nimport Foundation\n\npublic extension LemmyConnection {\n    func getReportCount(communityId: Int? = nil) async throws -> ReportUnreadCountSnapshot {\n        let response = try await performingForEndpoint { endpoint in\n            switch endpoint {\n            case .v3:\n                LemmyReportCountRequest(communityId: communityId)\n            case .v4:\n                throw ApiClientError.featureUnsupported\n            }\n        }\n        return try .init(from: response)\n    }\n    \n    func getPostReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false,\n        communityId: Int? = nil,\n        postId: Int? = nil\n    ) async throws -> [ReportSnapshot] {\n        let response = try await performingForEndpoint { _ in\n            LemmyListPostReportsRequest(\n                page: page,\n                limit: limit,\n                unresolvedOnly: unresolvedOnly,\n                communityId: communityId,\n                postId: postId\n            )\n        }\n        return try response.postReports.map { try .init(from: $0) }\n    }\n    \n    func getCommentReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false,\n        communityId: Int? = nil,\n        commentId: Int? = nil\n    ) async throws -> [ReportSnapshot] {\n        let response = try await performingForEndpoint { _ in\n            LemmyListCommentReportsRequest(\n                page: page,\n                limit: limit,\n                unresolvedOnly: unresolvedOnly,\n                communityId: communityId,\n                commentId: commentId\n            )\n        }\n        return try response.commentReports.map { try .init(from: $0) }\n    }\n    \n    func getMessageReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false\n    ) async throws -> [ReportSnapshot] {\n        let response = try await performingForEndpoint { _ in\n            LemmyListPmReportsRequest(\n                page: page,\n                limit: limit,\n                unresolvedOnly: unresolvedOnly\n            )\n        }\n        return try response.privateMessageReports.map { try .init(from: $0) }\n    }\n    \n    @discardableResult\n    func resolvePostReport(id: Int, resolved: Bool) async throws -> ReportSnapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyResolvePostReportRequest(endpoint: endpoint, reportId: id, resolved: resolved)\n        }\n        return try .init(from: response.postReportView)\n    }\n    \n    @discardableResult\n    func resolveCommentReport(id: Int, resolved: Bool) async throws -> ReportSnapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyResolveCommentReportRequest(endpoint: endpoint, reportId: id, resolved: resolved)\n        }\n        return try .init(from: response.commentReportView)\n    }\n    \n    @discardableResult\n    func resolveMessageReport(id: Int, resolved: Bool) async throws -> ReportSnapshot {\n        let response = try await performingForEndpoint { endpoint in\n            LemmyResolvePmReportRequest(endpoint: endpoint, reportId: id, resolved: resolved)\n        }\n        return try .init(from: response.privateMessageReportView)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/LemmyConnection/LemmyConnection.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-05.\n//\n\nimport Foundation\nimport Rest\n\npublic class LemmyConnection: InstanceConnection {\n    public static let softwareType: SiteSoftwareType = .lemmy\n    \n    let restClient = RestClient(errorType: ApiErrorResponse.self)\n    \n    enum LemmyConnectionError: Error {\n        case invalidSession\n    }\n    \n    struct Context {\n        let siteVersion: SiteVersion\n        let myPersonId: Int?\n    }\n    \n    struct RawContext {\n        let site: LemmyGetSiteResponse\n        let myUser: LemmyMyUserInfo?\n    }\n\n    public let baseUrl: URL\n    public var token: String?\n    \n    private var endpointMultiplexer: ConnectionMultiplexer<LemmyEndpointVersion> = .init {\n        // The order here matters! Lemmy 1.0 supports both v3 and v4.\n        // Putting v4 first in the array gives it priority.\n        [.v4, .v3]\n    }\n    private(set) var contextDataManager: SharedTaskManager<Context, RawContext> = .init()\n\n    public var fetchedVersion: SiteVersion? {\n        contextDataManager.fetchedValue?.siteVersion\n    }\n    \n    /// Returns the `fetchedVersion` if the version has already been fetched. Otherwise, waits until the version has been fetched before returning the received value.\n    public var version: SiteVersion {\n        get async throws {\n            try await contextDataManager.getValue().siteVersion\n        }\n    }\n    \n    public var myPersonId: Int? {\n        get async throws {\n            try await contextDataManager.getValue().myPersonId\n        }\n    }\n    \n    public var contextIsFetched: Bool {\n        contextDataManager.fetchedValue != nil\n    }\n    \n    public func ensureContextPresence() async throws {\n        try await contextDataManager.getValue()\n    }\n    \n    public required init(baseUrl: URL, token: String? = nil) {\n        self.baseUrl = baseUrl\n        self.token = token\n        contextDataManager.fetchTask = {\n            try await self.getRawContext()\n        }\n        contextDataManager.createValue = { response in\n            .init(siteVersion: .init(response.site.version), myPersonId: response.myUser?.localUserView.person.id)\n        }\n    }\n\n    public func updateToken(_ newToken: String) {\n        token = newToken\n    }\n\n    @discardableResult\n    func perform<Request: RestRequest>(\n        _ request: Request,\n        tokenOverride: String? = nil,\n        endpoint: LemmyEndpointVersion\n    ) async throws -> Request.Response {\n        let token = tokenOverride ?? token\n        do throws(RestError) {\n            return try await restClient.perform(\n                baseUrl: baseUrl,\n                request,\n                token: token,\n                encoderUserInfo: [.endpointVersion: endpoint]\n            )\n        } catch {\n            switch error {\n            case let RestError.response(response, statusCode: _):\n                if ApiErrorResponse(error: response).isNotLoggedIn {\n                    if token == nil {\n                        throw ApiClientError.notLoggedIn\n                    } else {\n                        throw LemmyConnectionError.invalidSession\n                    }\n                } else {\n                    throw ApiClientError(from: error)\n                }\n            default:\n                throw ApiClientError(from: error)\n            }\n        }\n    }\n    \n    // When this function is called, the `requestGenerator` will be called at least once,\n    // but may be called more than once.\n    func performingForEndpoint<Request: RestRequest>(\n        _ requestGenerator: @escaping (LemmyEndpointVersion) async throws -> Request\n    ) async throws -> Request.Response {\n        do {\n            return try await endpointMultiplexer.perform { endpoint in\n                try await self.perform(requestGenerator(endpoint), endpoint: endpoint)\n            }\n        } catch ConnectionMultiplexerError.allConnectionsFailed {\n            throw ApiClientError.serverError(statusCode: 404)\n        }\n    }\n    \n    // When this function is called, the `callback` will be called at least once,\n    // but may be called more than once.\n    func processingForEndpoint<Response>(\n        _ callback: @escaping (LemmyEndpointVersion) async throws -> Response\n    ) async throws -> Response {\n        do {\n            return try await endpointMultiplexer.perform { endpoint in\n                try await callback(endpoint)\n            }\n        } catch ConnectionMultiplexerError.allConnectionsFailed {\n            throw ApiClientError.serverError(statusCode: 404)\n        }\n    }\n    \n    #if DEBUG\n        func setMockContext(_ context: Context) {\n            contextDataManager.fetchedValue = context\n        }\n    #endif\n}\n\npublic extension CodingUserInfoKey {\n    static let endpointVersion = CodingUserInfoKey(rawValue: \"com.hanners.Mlem.endpointVersion\")!\n}\n\nenum LemmyEncodingError: Error {\n    case noEndpointVersionInUserInfo\n    case lemmyVoteShowBridge\n}\n\nextension Encoder {\n    var endpointVersion: LemmyEndpointVersion {\n        get throws {\n            if let endpoint = userInfo[.endpointVersion] as? LemmyEndpointVersion {\n                return endpoint\n            } else {\n                assertionFailure()\n                throw LemmyEncodingError.noEndpointVersionInUserInfo\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+Comment.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-06.\n//\n\nimport Foundation\n\npublic extension PieFedConnection {\n    func getComment(id: Int) async throws -> Comment2Snapshot {\n        let request = PieFedGetCommentRequest(id: id)\n        let response = try await perform(request)\n        return try .init(from: response.commentView)\n    }\n    \n    func getComment(url: URL) async throws -> Comment2Snapshot {\n        do {\n            let request = PieFedResolveObjectRequest(q: url.absoluteString)\n            let response = try await perform(request)\n            if let comment = response.comment {\n                return try .init(from: comment)\n            }\n        } catch let ApiClientError.response(response, _) where response.couldntFindObject {\n            throw ApiClientError.noEntityFound\n        }\n        throw ApiClientError.noEntityFound\n    }\n    \n    func getComments(\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment2Snapshot] {\n        guard let sort = sort.piefedCommentSortType, filter != .downvoted else {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedGetCommentsRequest(\n            type_: .all,\n            sort: sort,\n            maxDepth: maxDepth,\n            page: page,\n            limit: limit,\n            communityId: nil,\n            postId: nil,\n            parentId: nil,\n            personId: nil,\n            likedOnly: filter == .upvoted,\n            savedOnly: filter == .saved,\n            depthFirst: false\n        )\n        let response = try await perform(request)\n        return try response.comments.map { try .init(from: $0) }\n    }\n\n    func getComments(\n        postId: Int,\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment2Snapshot] {\n        guard let sort = sort.piefedCommentSortType, filter != .downvoted else {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedGetCommentsRequest(\n            type_: .all,\n            sort: sort,\n            maxDepth: maxDepth,\n            page: page,\n            limit: limit,\n            communityId: nil,\n            postId: postId,\n            parentId: nil,\n            personId: nil,\n            likedOnly: filter == .upvoted,\n            savedOnly: filter == .saved,\n            depthFirst: false\n        )\n        let response = try await perform(request)\n        return try response.comments.map { try .init(from: $0) }\n    }\n    \n    func getComments(\n        parentId: Int,\n        sort: CommentSortType,\n        page: Int,\n        maxDepth: Int? = nil,\n        limit: Int,\n        filter: GetContentFilter? = nil\n    ) async throws -> [Comment2Snapshot] {\n        guard let sort = sort.piefedCommentSortType, filter != .downvoted else {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedGetCommentsRequest(\n            type_: .all,\n            sort: sort,\n            maxDepth: maxDepth,\n            page: page,\n            limit: limit,\n            communityId: nil,\n            postId: nil,\n            parentId: parentId,\n            personId: nil,\n            likedOnly: filter == .upvoted,\n            savedOnly: filter == .saved,\n            depthFirst: false\n        )\n        let response = try await perform(request)\n        return try response.comments.map { try .init(from: $0) }\n    }\n\n    func getCommentHistory(\n        type: GetContentFilter,\n        page: Int?,\n        cursor: String?,\n        limit: Int\n    ) async throws -> (comments: [Comment2Snapshot], cursor: String?) {\n        guard type != .downvoted else {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedGetCommentsRequest(\n            type_: .all,\n            sort: nil,\n            maxDepth: nil,\n            page: page,\n            limit: limit,\n            communityId: nil,\n            postId: nil,\n            parentId: nil,\n            personId: nil,\n            likedOnly: type == .upvoted,\n            savedOnly: type == .saved,\n            depthFirst: false\n        )\n        let response = try await perform(request)\n        return try (\n            comments: response.comments.map { try .init(from: $0) },\n            cursor: nil\n        )\n    }\n    \n    // This method should be removed in favor of the below method once we drop support for versions before Lemmy 1.0\n    func searchComments(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: CommentSortType = .top(.allTime)\n    ) async throws -> [Comment2Snapshot] {\n        guard let sort = sort.piefedSortType else {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedSearchRequest(\n            q: query,\n            type_: .comments,\n            sort: sort,\n            listingType: filter.pieFedListingType,\n            page: page,\n            limit: limit,\n            communityName: nil,\n            communityId: communityId,\n            minimumUpvotes: nil,\n            nsfw: nil\n        )\n        let response = try await perform(request)\n        guard let comments = response.comments else {\n            throw ApiClientError.featureUnsupported\n        }\n        return try comments.map { try .init(from: $0) } \n    }\n    \n    func searchComments(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) async throws -> [Comment2Snapshot] {\n        throw ApiClientError.featureUnsupported\n    }\n\n    private func searchComments(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int?,\n        creatorId: Int?,\n        filter: ListingType,\n        legacySort: LemmySortType?,\n        sort: LemmySearchSortType?,\n        timeRangeSeconds: Int?\n    ) async throws -> [Comment2Snapshot] {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func voteOnComment(id: Int, score: ScoringOperation) async throws -> Comment2Snapshot {\n        let request = PieFedLikeCommentRequest(\n            commentId: id,\n            score: score.rawValue,\n            private: false,\n            emoji: nil\n        )\n        let response = try await perform(request)\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func saveComment(id: Int, save: Bool) async throws -> Comment2Snapshot {\n        let request = PieFedSaveCommentRequest(commentId: id, save: save)\n        let response = try await perform(request)\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func deleteComment(id: Int, delete: Bool) async throws -> Comment2Snapshot {\n        let request = PieFedDeleteCommentRequest(commentId: id, deleted: delete)\n        let response = try await perform(request)\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func editComment(\n        id: Int,\n        content: String,\n        languageId: Int?\n    ) async throws -> Comment2Snapshot {\n        let request = PieFedEditCommentRequest(\n            commentId: id,\n            body: content,\n            languageId: languageId,\n            distinguished: false\n        )\n        let response = try await perform(request)\n        return try .init(from: response.commentView)\n    }\n    \n    func replyToComment(\n        postId: Int,\n        parentId: Int?,\n        content: String,\n        languageId: Int? = nil\n    ) async throws -> Comment2Snapshot {\n        let request = PieFedCreateCommentRequest(\n            body: content,\n            postId: postId,\n            parentId: parentId,\n            languageId: languageId\n        )\n        let response = try await perform(request)\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func reportComment(id: Int, reason: String) async throws -> ReportSnapshot {\n        let request = PieFedCreateCommentReportRequest(\n            commentId: id,\n            reason: reason,\n            description: nil,\n            reportRemote: nil\n        )\n        let response = try await perform(request)\n        return try .init(from: response.commentReportView)\n    }\n    \n    func purgeComment(id: Int, reason: String?) async throws {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func removeComment(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Comment2Snapshot {\n        let request = PieFedRemoveCommentRequest(commentId: id, removed: remove, reason: reason)\n        let response = try await perform(request)\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func getCommentVotes(\n        id: Int,\n        page: Int = 1,\n        limit: Int = 20\n    ) async throws -> [PersonVoteSnapshot] {\n        let request = PieFedListCommentLikesRequest(commentId: id, page: page, limit: limit)\n        let response = try await perform(request)\n        return try response.commentLikes.map { try .init(from: $0) }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+Community.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-07.\n//\n\nimport Foundation\n\npublic extension PieFedConnection {\n    func getCommunity(id: Int) async throws -> Community3Snapshot {\n        let request = PieFedGetCommunityRequest(id: id, name: nil)\n        let response = try await perform(request)\n        return try .init(from: response)\n    }\n    \n    func getCommunity(url: URL) async throws -> Community2Snapshot {\n        do {\n            let request = PieFedResolveObjectRequest(q: url.absoluteString)\n            let response = try await perform(request)\n            if let community = response.community {\n                return try .init(from: community)\n            }\n        } catch let ApiClientError.response(response, _) where response.couldntFindObject {\n            throw ApiClientError.noEntityFound\n        }\n        throw ApiClientError.noEntityFound\n    }\n    \n    func searchCommunities(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        filter: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) async throws -> [Community2Snapshot] {\n        guard let sort = sort.pieFedSortType else {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedSearchRequest(\n            q: query,\n            type_: .communities,\n            sort: sort,\n            listingType: filter.pieFedListingType,\n            page: page,\n            limit: limit,\n            communityName: nil,\n            communityId: nil,\n            minimumUpvotes: nil,\n            nsfw: nil\n        )\n        let response = try await perform(request)\n        return try response.communities.map { try .init(from: $0) }\n    }\n\n    func editCommunityDescription(id: Int, newValue: String?) async throws -> Community2Snapshot {\n        let request = PieFedEditCommunityRequest(\n            id: id,\n            title: nil,\n            description: newValue,\n            rules: nil,\n            iconUrl: nil,\n            bannerUrl: nil,\n            nsfw: nil,\n            restrictedToMods: nil,\n            localOnly: nil,\n            discussionLanguages: nil,\n            communityId: id,\n            questionAnswer: nil\n        )\n        let response = try await perform(request)\n        return try .init(from: response.communityView)\n    }\n    \n    @discardableResult\n    func getSubscriptionList(page: Int, limit: Int) async throws -> [Community2Snapshot] {\n        let request = PieFedListCommunitiesRequest(\n            type_: .subscribed,\n            sort: nil,\n            showNsfw: true,\n            page: page,\n            limit: limit\n        )\n        let response = try await perform(request)\n        return try response.communities.map { try .init(from: $0) }\n    }\n    \n    @discardableResult\n    func subscribeToCommunity(id: Int, subscribe: Bool) async throws -> Community2Snapshot {\n        let request = PieFedFollowCommunityRequest(communityId: id, follow: subscribe)\n        let response = try await perform(request)\n        return try .init(from: response.communityView)\n    }\n    \n    @discardableResult\n    func blockCommunity(id: Int, block: Bool) async throws -> Community2Snapshot {\n        let request = PieFedBlockCommunityRequest(communityId: id, block: block)\n        let response = try await perform(request)\n        return try .init(from: response.communityView)\n    }\n    \n    @discardableResult\n    func removeCommunity(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Community2Snapshot {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func purgeCommunity(id: Int, reason: String?) async throws {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func addModerator(\n        communityId: Int,\n        personId: Int,\n        added: Bool\n    ) async throws -> (moderators: [Person1Snapshot], community: Community1Snapshot) {\n        let request = PieFedAddModToCommunityRequest(communityId: communityId, personId: personId, added: added)\n        let response = try await perform(request)\n        let moderators: [Person1Snapshot] = try response.moderators.map { try .init(from: $0.moderator) }\n        \n        guard let first = response.moderators.first else {\n            throw ApiClientError.unsuccessful\n        }\n        let community: Community1Snapshot = try .init(from: first.community)\n        return (\n            moderators: moderators,\n            community: community\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+Feature.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-13.\n//\n\nimport Foundation\n\npublic extension PieFedConnection {\n    func supports(_ feature: Feature) async throws -> Bool {\n        try await Self.supports(feature, version: version)\n    }\n    \n    func supports(_ feature: Feature, defaultValue: Bool) -> Bool {\n        if let fetchedVersion {\n            return Self.supports(feature, version: fetchedVersion)\n        } else {\n            return defaultValue\n        }\n    }\n\n    static func supports(\n        _ feature: Feature,\n        version: SiteVersion\n    ) -> Bool {\n        switch feature {\n        case let .postSortType(sort):\n            version >= sort.minimumVersion\n        case let .commentSortType(sort):\n            version >= sort.minimumVersion\n        case let .searchSortType(sort):\n            version >= sort.minimumVersion\n        case let .sortTimeRange(timeRange):\n            version >= timeRange.minimumVersion\n        case let .listingType(listingType):\n            listingType.pieFedListingType != nil\n        case .viewCommunityActiveUsers, .viewMentionsAndPrivateMessages, .editAndDeletePrivateMessages, .autoMarkPostReadOnInteract:\n            version >= .v1_1_0\n        case .editProfile, .viewVotes, .undeletePrivateMessages:\n            version >= .v1_2_0\n        case .banFromCommunity, .editCommunityDescription:\n            version >= .v1_3_0\n        case .searchLocalPeople, .searchLocalCommunities, .blockInstances:\n            // These features were not necessarily added in 1.3.\n            // Rather, we have only tested them on 1.3 and so are\n            // restricting them to that version.\n            version >= .v1_3_0\n        case .commentSearch:\n            version >= .v1_3_0\n        case .userNotes, .searchLocalComments, .fetchLinkMetadata:\n            version >= .v1_4_0\n        case .moderatorSetNsfw: true\n        default: false\n        }\n    }\n}\n\nprivate extension SiteVersion {\n    static let v1_0_0: Self = .init(\"1.0.0\")\n    static let v1_1_0: Self = .init(\"1.1.0\")\n    static let v1_2_0: Self = .init(\"1.2.0\")\n    static let v1_3_0: Self = .init(\"1.3.0\")\n    static let v1_4_0: Self = .init(\"1.4.0\")\n}\n\nprivate extension PostSortType {\n    var minimumVersion: SiteVersion {\n        switch self {\n        case .active: .infinity\n        case .hot: .zero\n        case .new: .zero\n        case .old: .v1_3_0\n        case .mostComments: .infinity\n        case .newComments: .zero\n        case .controversial: .infinity\n        case .scaled: .zero\n        case let .top(timeRange): timeRange.minimumVersion\n        }\n    }\n}\n\nprivate extension CommentSortType {\n    var minimumVersion: SiteVersion {\n        switch self {\n        case .new: .zero\n        case .old: .zero\n        case .hot: .zero\n        case .controversial: .v1_4_0\n        case let .top(timeRange): timeRange == .allTime ? .zero : .infinity\n        }\n    }\n}\n\nprivate extension SearchSortType {\n    var minimumVersion: SiteVersion {\n        switch self {\n        case .new: .zero\n        case .old: .infinity\n        case let .top(timeRange): timeRange.minimumVersion\n        }\n    }\n}\n\nprivate extension SortTimeRange {\n    var minimumVersion: SiteVersion {\n        switch self {\n        case .allTime: .v1_1_0\n        case let .limited(timeInterval): LegacySortTimeRangeLimit(timeInterval)?.minimumVersion ?? .infinity\n        }\n    }\n}\n\nprivate extension LegacySortTimeRangeLimit {\n    var minimumVersion: SiteVersion {\n        switch self {\n        case .threeMonth, .sixMonth, .nineMonth, .year: .v1_1_0\n        default: .zero\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+General.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-07.\n//\n\nimport Foundation\n\npublic extension PieFedConnection {\n    func getAccountToken(\n        usernameOrEmail: String,\n        password: String,\n        totpToken: String?\n    ) async throws -> String {\n        if totpToken != nil {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedUserLoginRequest(username: usernameOrEmail, password: password)\n        let response = try await perform(request)\n        guard let jwt = response.jwt else {\n            throw ApiClientError.notLoggedIn\n        }\n        return jwt\n    }\n    \n    func getUsernameFromToken(token: String) async throws -> String {\n        let request = PieFedGetSiteRequest()\n        let response = try await perform(request, tokenOverride: token)\n        if let name = response.myUser?.localUserView.person.userName {\n            return name\n        }\n        throw ApiClientError.notLoggedIn\n    }\n    \n    func signUp(\n        username: String,\n        password: String,\n        confirmPassword: String,\n        showNsfw: Bool,\n        email: String?,\n        captcha: Captcha?,\n        captchaAnswer: String?,\n        applicationQuestionResponse: String?\n    ) async throws -> SignUpResponse {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func changePassword(\n        newPassword: String,\n        confirmNewPassword: String,\n        oldPassword: String\n    ) async throws -> String {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func getCaptcha() async throws -> Captcha {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func resolve(url: URL) async throws -> ResolvedContent {\n        let request = PieFedResolveObjectRequest(q: url.absoluteString)\n        let response = try await perform(request)\n        return try .init(from: response)\n    }\n    \n    func getBlocked() async throws -> (people: [Person1Snapshot], communities: [Community1Snapshot], instances: [Instance1Snapshot]) {\n        let request = PieFedGetSiteRequest()\n        let response = try await perform(request)\n        guard let myUser = response.myUser else { return ([], [], []) }\n        \n        return try (\n            people: myUser.personBlocks.map { try .init(from: $0.target) },\n            communities: myUser.communityBlocks.map { try .init(from: $0.community) },\n            instances: myUser.instanceBlocks.compactMap(\\.site).map { try .init(from: $0) }\n        )\n    }\n    \n    func getModlog(\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        moderatorId: Int? = nil,\n        subjectPersonId: Int? = nil,\n        postId: Int? = nil,\n        commentId: Int? = nil,\n        type: ModlogEntryType? = nil\n    ) async throws -> [ModlogEntrySnapshot] {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func getPostLink(url: URL) async throws -> PostLink {\n        guard try await self.supports(.fetchLinkMetadata) else {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedGetSiteMetadataRequest(url: url.absoluteString)\n        let response = try await perform(request)\n        guard let imageUrl = response.metadata.image.map(URL.init(string:)) else {\n            throw ApiClientError.unsuccessful\n        }\n        return .init(\n            content: url,\n            thumbnail: imageUrl,\n            label: response.metadata.title ?? url.absoluteString\n        )\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+Image.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-05.\n//\n\nimport Foundation\nimport Rest\n\npublic extension PieFedConnection {\n    func uploadImage(\n        _ imageData: Data,\n        fileExtension: String,\n        onProgress progressCallback: @escaping (_ progress: Double) -> Void = { _ in }\n    ) async throws -> ImageUpload1Snapshot {\n        guard let token else { throw ApiClientError.notLoggedIn }\n        var request = mlemUrlRequest(url: baseUrl.appending(path: \"api/alpha/upload/image\"))\n        request.httpMethod = \"POST\"\n        \n        let boundary = UUID().uuidString\n        request.setValue(\"multipart/form-data; boundary=\\(boundary)\", forHTTPHeaderField: \"Content-Type\")\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n        \n        let encodedData = createMultiPartForm(\n            boundary: boundary,\n            contentType: \"application/octet-stream\",\n            name: \"file\",\n            fileName: \"image.\\(fileExtension)\",\n            imageData: imageData,\n            auth: token\n        )\n        \n        let (data, _) = try await restClient.urlSession.upload(\n            for: request,\n            from: encodedData,\n            delegate: ImageUploadDelegate(callback: progressCallback)\n        )\n        \n        do {\n            let response = try JSONDecoder.defaultDecoder.decode(PieFedImageUploadResponse.self, from: data)\n            return .init(from: response)\n        } catch DecodingError.dataCorrupted {\n            let text = String(decoding: data, as: UTF8.self)\n            if text.contains(\"413 Request Entity Too Large\") {\n                throw ApiClientError.imageTooLarge\n            }\n            throw ApiClientError.decoding(data, nil)\n        }\n    }\n    \n    func deleteImage(alias: String, deleteToken: String) async throws {\n        throw ApiClientError.featureUnsupported\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+Inbox.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-08.\n//\n\nimport Foundation\n\npublic extension PieFedConnection {\n    func getMessages(\n        creatorId: Int? = nil,\n        page: Int,\n        limit: Int,\n        unreadOnly: Bool = false\n    ) async throws -> [Message2Snapshot] {\n        if let creatorId {\n            if unreadOnly {\n                throw ApiClientError.featureUnsupported\n            }\n            let request = PieFedGetPrivateMessagesConversationRequest(\n                page: page,\n                limit: limit,\n                personId: creatorId,\n                conversationId: nil\n            )\n            let response = try await perform(request)\n            return try response.privateMessages.map { try .init(from: $0) }\n        } else {\n            let request = PieFedListPrivateMessagesRequest(\n                unreadOnly: unreadOnly,\n                page: page,\n                limit: limit,\n                creatorId: nil\n            )\n            let response = try await perform(request)\n            return try response.privateMessages.map { try .init(from: $0) }\n        }\n    }\n    \n    func getReplyNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?) {\n        let request = PieFedGetRepliesRequest(\n            sort: .new,\n            page: page,\n            limit: limit,\n            unreadOnly: unreadOnly\n        )\n        let response = try await perform(request)\n        return try (notifications: response.replies.map { try .init(from: $0, isMention: false) }, cursor: nil)\n    }\n\n    func getMentionNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?) {\n        let request = PieFedGetMentionsRequest(\n            sort: .new,\n            page: page,\n            limit: limit,\n            unreadOnly: unreadOnly\n        )\n        let response = try await perform(request)\n        return try (notifications: response.replies.map { try .init(from: $0, isMention: true) }, cursor: nil)\n    }\n\n    func getMessageNotifications(\n        page: Int?,\n        cursor: String?,\n        limit: Int,\n        unreadOnly: Bool\n    ) async throws -> (notifications: [InboxNotificationSnapshot], cursor: String?) {\n        let request = PieFedListPrivateMessagesRequest(\n            unreadOnly: unreadOnly,\n            page: page,\n            limit: limit,\n            creatorId: nil\n        )\n        let response = try await perform(request)\n        return try (notifications: response.privateMessages.map { try .init(from: $0) }, cursor: nil)\n    }\n    \n    func markNotificationAsRead(\n        type: InboxNotificationContentType,\n        id: Int,\n        contentId: Int,\n        read: Bool\n    ) async throws {\n        switch type {\n        case .reply:\n            try await self.markReplyAsRead(id: contentId, read: read)\n        case .mention:\n            try await self.markMentionAsRead(id: contentId, read: read)\n        case .message:\n            try await self.markMessageAsRead(id: contentId, read: read)\n        }\n    }\n\n    private func markReplyAsRead(id: Int, read: Bool = true) async throws {\n        let request = PieFedMarkReplyAsReadRequest(commentReplyId: id, read: read)\n        try await perform(request)\n    }\n    \n    private func markMentionAsRead(id: Int, read: Bool = true) async throws {\n        let request = PieFedMarkReplyAsReadRequest(commentReplyId: id, read: read)\n        try await perform(request)\n    }\n    \n    private func markMessageAsRead(id: Int, read: Bool = true) async throws {\n        let request = PieFedMarkPrivateMessageAsReadRequest(privateMessageId: id, read: read)\n        try await perform(request)\n    }\n    \n    func markAllAsRead() async throws {\n        let request = PieFedMarkAllRepliesReadRequest()\n        try await perform(request)\n    }\n    \n    func getPersonalUnreadCount() async throws -> PersonalUnreadCountSnapshot {\n        let request = PieFedGetUnreadCountRequest()\n        let response = try await perform(request)\n        return try .init(from: response)\n    }\n    \n    func createMessage(personId: Int, content: String) async throws -> Message2Snapshot {\n        let request = PieFedCreatePrivateMessageRequest(content: content, recipientId: personId)\n        let response = try await perform(request)\n        return try .init(from: response.privateMessageView)\n    }\n    \n    @discardableResult\n    func editMessage(id: Int, content: String) async throws -> Message2Snapshot {\n        let request = PieFedEditPrivateMessageRequest(privateMessageId: id, content: content)\n        let response = try await perform(request)\n        return try .init(from: response.privateMessageView)\n    }\n    \n    @discardableResult\n    func reportMessage(id: Int, reason: String) async throws -> ReportSnapshot {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func deleteMessage(id: Int, delete: Bool) async throws -> Message2Snapshot {\n        let request = PieFedDeletePrivateMessageRequest(\n            messageId: id,\n            deleted: delete,\n            privateMessageId: id\n        )\n        let response = try await perform(request)\n        return try .init(from: response.privateMessageView)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+Instance.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-08.\n//\n\nimport Foundation\n\npublic extension PieFedConnection {\n    func getMyInstance() async throws -> Instance3Snapshot {\n        let response = try await rawGetMyPersonWithContext()\n        return try .init(pieFed: response.0, lemmy: response.1)\n    }\n    \n    func getFederatedInstances() async throws -> FederationPolicy {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func blockInstance(instanceId: Int, block: Bool) async throws {\n        let request = PieFedBlockInstanceRequest(\n            instanceId: instanceId,\n            block: block\n        )\n        try await perform(request)\n    }\n    \n    @discardableResult\n    func addAdmin(personId: Int, added: Bool) async throws -> [Person2Snapshot] {\n        throw ApiClientError.featureUnsupported\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+Person.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-06.\n//\n\nimport Foundation\n\npublic extension PieFedConnection {\n    func getPerson(id: Int) async throws -> Person3Snapshot {\n        let request = PieFedGetPersonDetailsRequest(\n            personId: id,\n            username: nil,\n            sort: .new,\n            page: 1,\n            limit: 1,\n            communityId: nil,\n            savedOnly: nil,\n            includeContent: false\n        )\n        let response = try await perform(request)\n        return try .init(from: response)\n    }\n    \n    func getPerson(url: URL) async throws -> Person2Snapshot {\n        do {\n            let request = PieFedResolveObjectRequest(q: url.absoluteString)\n            let response = try await perform(request)\n            if let person = response.person {\n                return try .init(from: person)\n            }\n        } catch let ApiClientError.response(response, _) where response.couldntFindObject {\n            throw ApiClientError.noEntityFound\n        }\n        throw ApiClientError.noEntityFound\n    }\n    \n    func getPerson(username: String) async throws -> Person3Snapshot {\n        let request = PieFedGetPersonDetailsRequest(\n            personId: nil,\n            username: username,\n            sort: .new,\n            page: 1,\n            limit: 1,\n            communityId: nil,\n            savedOnly: nil,\n            includeContent: false\n        )\n        let response = try await perform(request)\n        return try .init(from: response)\n    }\n    \n    /// `filter` can be set to `.local` from 0.19.4 onwards.\n    func searchPeople(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        filter: ListingType = .all,\n        sort: SearchSortType = .top(.allTime)\n    ) async throws -> [Person2Snapshot] {\n        guard let sort = sort.pieFedSortType else {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedSearchRequest(\n            q: query,\n            type_: .users,\n            sort: sort,\n            listingType: filter.pieFedListingType,\n            page: page,\n            limit: limit,\n            communityName: nil,\n            communityId: nil,\n            minimumUpvotes: nil,\n            nsfw: nil\n        )\n        let response = try await perform(request)\n        return try response.users.map { try .init(from: $0) }\n    }\n    \n    @discardableResult\n    func blockPerson(id: Int, block: Bool) async throws -> Person2Snapshot {\n        let request = PieFedBlockPersonRequest(personId: id, block: block)\n        let response = try await perform(request)\n        return try .init(from: response.personView)\n    }\n    \n    @discardableResult\n    func banPersonFromCommunity(\n        personId: Int,\n        communityId: Int,\n        ban: Bool,\n        removeContent: Bool,\n        reason: String?,\n        expires: Date? = nil\n    ) async throws -> Person1Snapshot {\n        // Explicit check because the endpoint exists before 1.3, but the date\n        // formats are different. Don't want to send a broken ban request.\n        if try await !supports(.banFromCommunity) {\n            throw ApiClientError.featureUnsupported\n        }\n\n        if ban {\n            let request = PieFedModerateCommunityBanRequest(\n                communityId: communityId,\n                userId: personId,\n                reason: reason ?? \"\",\n                expiredAt: nil,\n                expiresAt: expires,\n                permanent: expires == nil\n            )\n            let response = try await perform(request)\n            return try .init(from: response.bannedUser)\n        } else {\n            let request = PieFedModerateCommunityUnBanRequest(\n                communityId: communityId,\n                userId: personId\n            )\n            let response = try await perform(request)\n            return try .init(from: response.bannedUser)\n        }\n    }\n    \n    @discardableResult\n    func banPersonFromInstance(\n        personId: Int,\n        ban: Bool,\n        removeContent: Bool,\n        reason: String?,\n        expires: Date? = nil\n    ) async throws -> Person2Snapshot {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func purgePerson(id: Int, reason: String?) async throws {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func getContent(\n        authorId id: Int,\n        sort: PostSortType,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool? = nil,\n        communityId: Int? = nil\n    ) async throws -> (person: Person3Snapshot, posts: [Post2Snapshot], comments: [Comment2Snapshot]) {\n        let request = PieFedGetPersonDetailsRequest(\n            personId: id,\n            username: nil,\n            sort: .new,\n            page: page,\n            limit: limit,\n            communityId: nil,\n            savedOnly: nil,\n            includeContent: true\n        )\n        let response = try await perform(request)\n        return try (\n            person: .init(from: response),\n            posts: response.posts.map { try .init(from: $0) },\n            comments: response.comments.map { try .init(from: $0) }\n        )\n    }\n    \n    // Returns a raw API type. For use inside PieFedConnection only\n    internal func rawGetMyPerson() async throws -> (PieFedGetSiteResponse, PieFedLemmyCompatibleSiteResponse) {\n        async let pieFedResponse = await perform(PieFedGetSiteRequest())\n        async let lemmyResponse = await perform(PieFedLemmyCompatibleGetSiteRequest())\n        return try await (pieFedResponse, lemmyResponse)\n    }\n    \n    // Calls rawGetMyPerson, but if there's already a task running in the `contextDataManager` uses that instead.\n    internal func rawGetMyPersonWithContext() async throws -> (PieFedGetSiteResponse, PieFedLemmyCompatibleSiteResponse) {\n        if let ongoingTask = contextDataManager.ongoingTask {\n            return try await ongoingTask.result.get()\n        } else {\n            let task = Task { try await rawGetMyPerson() }\n            Task.detached {\n                _ = try await self.contextDataManager.getValue(task: task)\n            }\n            return try await task.result.get()\n        }\n    }\n    \n    func getMyPerson() async throws -> (person: Person4Snapshot?, instance: Instance3Snapshot, blocks: BlockListSnapshot?) {\n        let response = try await rawGetMyPersonWithContext()\n        var person: Person4Snapshot?\n        var blocks: BlockListSnapshot?\n        if let myUser = response.0.myUser {\n            person = try .init(from: myUser)\n            blocks = .init(from: myUser)\n        }\n        \n        return try (\n            person: person,\n            instance: .init(pieFed: response.0, lemmy: response.1),\n            blocks: blocks\n        )\n    }\n    \n    func deleteAccount(password: String, deleteContent: Bool) async throws {\n        throw ApiClientError.featureUnsupported\n    }\n\n    func editNote(id: Int, content: String?) async throws {\n        let request = PieFedUserSetNoteRequest(personId: id, note: content ?? \"\")\n        try await perform(request)\n    }\n    \n    func editProfile(details: ProfileDetails) async throws {\n        let request = PieFedSaveUserSettingsRequest(\n            showNsfw: nil,\n            showReadPosts: nil,\n            bio: details.description,\n            avatar: details.avatar?.absoluteString ?? \"\",\n            cover: details.banner?.absoluteString ?? \"\",\n            defaultCommentSortType: nil,\n            defaultSortType: nil,\n            showNsfl: nil,\n            extraFields: nil,\n            acceptPrivateMessages: nil,\n            bot: nil,\n            botVisibility: nil,\n            communityKeywordFilter: nil,\n            emailUnread: nil,\n            federateVotes: nil,\n            feedAutoFollow: nil,\n            feedAutoLeave: nil,\n            hideLowQuality: nil,\n            indexable: nil,\n            newsletter: nil,\n            nsflVisibility: nil,\n            nsfwVisibility: nil,\n            genaiVisibility: nil,\n            replyCollapseThreshold: nil,\n            replyHideThreshold: nil,\n            searchable: nil\n        )\n        try await perform(request)\n    }\n\n    func editAccountSettings(\n        showNsfw: Bool?,\n        showScores: Bool?,\n        theme: String?,\n        defaultListingType: ListingType?,\n        interfaceLanguage: String?,\n        avatar: String?,\n        banner: String?,\n        displayName: String?,\n        email: String?,\n        bio: String?,\n        matrixUserId: String?,\n        showAvatars: Bool?,\n        sendNotificationsToEmail: Bool?,\n        botAccount: Bool?,\n        showBotAccounts: Bool?,\n        showReadPosts: Bool?,\n        discussionLanguages: [Int]?,\n        openLinksInNewTab: Bool?,\n        blurNsfw: Bool?,\n        autoExpand: Bool?,\n        infiniteScrollEnabled: Bool?,\n        postListingMode: PostFeedViewMode?,\n        enableKeyboardNavigation: Bool?,\n        enableAnimatedImages: Bool?,\n        collapseBotComments: Bool?,\n        showUpvotes: Bool?,\n        showDownvotes: Bool?,\n        showUpvotePercentage: Bool?\n    ) async throws {\n        throw ApiClientError.featureUnsupported\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+Post.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-05.\n//\n\nimport Foundation\n\npublic extension PieFedConnection {\n    func getPosts(\n        communityId: Int,\n        sort: PostSortType,\n        page: Int,\n        cursor: String?,\n        limit: Int,\n        filter: GetContentFilter? = nil,\n        showHidden: Bool = false\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?) {\n        if filter == .downvoted {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedListPostsRequest(\n            type_: nil,\n            sort: sort.pieFedSortType,\n            pageCursor: page,\n            limit: limit,\n            communityId: communityId,\n            personId: nil,\n            communityName: nil,\n            likedOnly: filter == .upvoted,\n            savedOnly: filter == .saved,\n            q: nil,\n            page: page,\n            feedId: nil,\n            topicId: nil,\n            ignoreSticky: nil\n        )\n        let response = try await perform(request)\n        let posts: [Post2Snapshot] = try response.posts.map { try .init(from: $0) }\n        return (posts: posts, cursor: nil)\n    }\n    \n    func getPosts(\n        feed: ListingType,\n        sort: PostSortType,\n        page: Int,\n        cursor: String?,\n        limit: Int,\n        filter: GetContentFilter? = nil,\n        showHidden: Bool = false\n    ) async throws -> (posts: [Post2Snapshot], cursor: String?) {\n        if filter == .downvoted || showHidden {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedListPostsRequest(\n            type_: feed.pieFedListingType,\n            sort: sort.pieFedSortType,\n            pageCursor: page,\n            limit: limit,\n            communityId: nil,\n            personId: nil,\n            communityName: nil,\n            likedOnly: filter == .upvoted,\n            savedOnly: filter == .saved,\n            q: nil,\n            page: page,\n            feedId: nil,\n            topicId: nil,\n            ignoreSticky: nil\n        )\n        let response = try await perform(request)\n        let posts: [Post2Snapshot] = try response.posts.map { try .init(from: $0) }\n        return (posts: posts, cursor: nil)\n    }\n\n    func getPosts(\n        personId: Int,\n        communityId: Int? = nil,\n        sort: PostSortType = .new,\n        page: Int,\n        limit: Int,\n        savedOnly: Bool = false\n    ) async throws -> (person: Person3Snapshot, posts: [Post2Snapshot]) {\n        throw ApiClientError.featureUnsupported\n    }\n\n    func getPostHistory(\n        type: GetContentFilter,\n        page: Int?,\n        cursor: String?,\n        limit: Int \n    ) async throws -> (posts: [Post2Snapshot], cursor: String?) {\n        guard type != .downvoted else {\n            throw ApiClientError.featureUnsupported\n        }\n        // PieFed doesn't support cursors so we need to fake it here\n\n        let pageNumber = (cursor.map(Int.init) ?? nil) ?? 1\n\n        let request = PieFedListPostsRequest(\n            type_: nil,\n            sort: .new,\n            pageCursor: pageNumber,\n            limit: limit,\n            communityId: nil,\n            personId: nil,\n            communityName: nil,\n            likedOnly: type == .upvoted,\n            savedOnly: type == .saved,\n            q: nil,\n            page: pageNumber,\n            feedId: nil,\n            topicId: nil,\n            ignoreSticky: nil\n        )\n        let response = try await perform(request)\n        let posts: [Post2Snapshot] = try response.posts.map { try .init(from: $0) }\n\n        return (posts: posts, cursor: String(pageNumber+1))\n    }\n\n    func getPost(id: Int) async throws -> Post3Snapshot {\n        let request = PieFedGetPostRequest(id: id, commentId: nil)\n        let response = try await perform(request)\n        return try .init(from: response)\n    }\n    \n    func getPost(url: URL) async throws -> Post2Snapshot {\n        do {\n            let request = PieFedResolveObjectRequest(q: url.absoluteString)\n            let response = try await perform(request)\n            if let post = response.post {\n                return try .init(from: post)\n            }\n        } catch let ApiClientError.response(response, _) where response.couldntFindObject {\n            throw ApiClientError.noEntityFound\n        }\n        throw ApiClientError.noEntityFound\n    }\n    \n    // This method should be removed in favor of the below method once we drop support for versions before Lemmy 1.0\n    func searchPosts(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: PostSortType\n    ) async throws -> [Post2Snapshot] {\n        guard let sort = sort.pieFedSortType else {\n            throw ApiClientError.featureUnsupported\n        }\n        if communityId != nil || creatorId != nil {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedSearchRequest(\n            q: query,\n            type_: .posts,\n            sort: sort,\n            listingType: filter.pieFedListingType,\n            page: page,\n            limit: limit,\n            communityName: nil,\n            communityId: communityId,\n            minimumUpvotes: nil,\n            nsfw: nil\n        )\n        let response = try await perform(request)\n        return try response.posts.map { try .init(from: $0) }\n    }\n    \n    func searchPosts(\n        query: String,\n        page: Int = 1,\n        limit: Int = 20,\n        communityId: Int? = nil,\n        creatorId: Int? = nil,\n        filter: ListingType = .all,\n        sort: SearchSortType\n    ) async throws -> [Post2Snapshot] {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    private func searchPosts(\n        query: String,\n        page: Int,\n        limit: Int,\n        communityId: Int?,\n        creatorId: Int?,\n        filter: ListingType,\n        legacySort: LemmySortType?,\n        sort: LemmySearchSortType?,\n        timeRangeSeconds: Int?\n    ) async throws -> [Post2Snapshot] {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func markPostsAsRead(ids: Set<Int>, read: Bool) async throws {\n        let request = PieFedMarkPostAsReadRequest(postIds: Array(ids), postId: nil, read: read)\n        try await perform(request)\n    }\n    \n    func markPostAsRead(id: Int, read: Bool) async throws {\n        let request = PieFedMarkPostAsReadRequest(postIds: nil, postId: id, read: read)\n        try await perform(request)\n    }\n    \n    @discardableResult\n    func voteOnPost(id: Int, score: ScoringOperation) async throws -> Post2Snapshot {\n        let request = PieFedLikePostRequest(\n            postId: id,\n            score: score.rawValue,\n            private: nil,\n            emoji: nil\n        )\n        async let response = perform(request)\n        if !supports(.autoMarkPostReadOnInteract, defaultValue: false) {\n            try await markPostAsRead(id: id, read: true)\n            return try await .init(from: response.postView, overrideRead: true)\n        }\n        return try await .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func savePost(id: Int, save: Bool) async throws -> Post2Snapshot {\n        let request = PieFedSavePostRequest(postId: id, save: save)\n        async let response = try await perform(request)\n        if !supports(.autoMarkPostReadOnInteract, defaultValue: false) {\n            try await markPostAsRead(id: id, read: true)\n            return try await .init(from: response.postView, overrideRead: true)\n        }\n        return try await .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func deletePost(id: Int, delete: Bool) async throws -> Post2Snapshot {\n        let request = PieFedDeletePostRequest(postId: id, deleted: delete)\n        let response = try await perform(request)\n        return try .init(from: response.postView)\n    }\n    \n    func hidePost(id: Int, hide: Bool) async throws {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func createPost(\n        communityId: Int,\n        title: String,\n        content: String? = nil,\n        linkUrl: URL? = nil,\n        altText: String? = nil,\n        thumbnail: URL? = nil,\n        nsfw: Bool,\n        languageId: Int? = nil\n    ) async throws -> Post2Snapshot {\n        if thumbnail != nil || altText != nil {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedCreatePostRequest(\n            title: title,\n            communityId: communityId,\n            url: linkUrl,\n            body: content,\n            nsfw: nsfw,\n            languageId: languageId,\n            altText: altText,\n            aiGenerated: nil,\n            event: nil,\n            poll: nil,\n        )\n        let response = try await perform(request)\n        return try .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func editPost(\n        id: Int,\n        title: String,\n        content: String? = nil,\n        linkUrl: URL? = nil,\n        altText: String? = nil,\n        thumbnail: URL? = nil,\n        nsfw: Bool,\n        languageId: Int? = nil\n    ) async throws -> Post2Snapshot {\n        if thumbnail != nil || altText != nil {\n            throw ApiClientError.featureUnsupported\n        }\n        let request = PieFedEditPostRequest(\n            postId: id,\n            title: title,\n            url: linkUrl,\n            body: content,\n            nsfw: nsfw,\n            languageId: languageId,\n            altText: altText,\n            event: nil,\n            poll: nil,\n            tags: nil,\n            flair: nil\n        )\n        let response = try await perform(request)\n        return try .init(from: response.postView)\n    }\n    \n    func replyToPost(\n        id: Int,\n        content: String,\n        languageId: Int? = nil\n    ) async throws -> Comment2Snapshot {\n        let request = PieFedCreateCommentRequest(\n            body: content,\n            postId: id,\n            parentId: nil,\n            languageId: languageId\n        )\n        let response = try await perform(request)\n        return try .init(from: response.commentView)\n    }\n    \n    @discardableResult\n    func reportPost(id: Int, reason: String) async throws -> ReportSnapshot {\n        let request = PieFedCreatePostReportRequest(\n            postId: id,\n            reason: reason,\n            description: nil,\n            reportRemote: true\n        )\n        let response = try await perform(request)\n        return try .init(from: response.postReportView)\n    }\n    \n    func purgePost(id: Int, reason: String?) async throws {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func removePost(\n        id: Int,\n        remove: Bool,\n        reason: String?\n    ) async throws -> Post2Snapshot {\n        let request = PieFedRemovePostRequest(postId: id, removed: remove, reason: reason)\n        let response = try await perform(request)\n        return try .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func pinPost(\n        id: Int,\n        pin: Bool,\n        to target: PostFeatureType\n    ) async throws -> Post2Snapshot {\n        let request = PieFedFeaturePostRequest(\n            postId: id,\n            featured: pin,\n            featureType: target.piefedPostFeatureType\n        )\n        let response = try await perform(request)\n        return try .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func lockPost(id: Int, lock: Bool) async throws -> Post2Snapshot {\n        let request = PieFedLockPostRequest(postId: id, locked: lock)\n        let response = try await perform(request)\n        return try .init(from: response.postView)\n    }\n    \n    @discardableResult\n    func setPostNsfw(id: Int, nsfw: Bool) async throws -> Post1Snapshot {\n        let request = PieFedModerateCommunityPostNsfwRequest(postId: id, nsfwStatus: nsfw)\n        let response = try await perform(request)\n        return try .init(from: response.post)\n    }\n\n    @discardableResult\n    func getPostVotes(\n        id: Int,\n        page: Int = 1,\n        limit: Int = 20\n    ) async throws -> [PersonVoteSnapshot] {\n        let request = PieFedListPostLikesRequest(postId: id, page: page, limit: limit)\n        let response = try await perform(request)\n        return try response.postLikes.map { try .init(from: $0) }\n    }\n\n    @discardableResult\n    func voteInPoll(postId: Int, choiceIds: Set<Int>) async throws -> Post2Snapshot {\n        let request = PieFedPollVoteRequest(postId: postId, choiceId: Array(choiceIds))\n        let response = try await perform(request)\n        return try .init(from: response.postView)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+RegistrationApplication.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-08.\n//\n\nimport Foundation\n\npublic extension PieFedConnection {\n    func getRegistrationApplicationCount() async throws -> Int {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func getRegistrationApplications(\n        page: Int = 1,\n        limit: Int = 20,\n        unreadOnly: Bool = false\n    ) async throws -> [RegistrationApplicationSnapshot] {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func approveRegistrationApplication(id: Int) async throws -> RegistrationApplicationSnapshot {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func denyRegistrationApplication(id: Int, reason: String?) async throws -> RegistrationApplicationSnapshot {\n        throw ApiClientError.featureUnsupported\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection+Report.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-08.\n//\n\nimport Foundation\n\npublic extension PieFedConnection {\n    func getReportCount(communityId: Int? = nil) async throws -> ReportUnreadCountSnapshot {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func getPostReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false,\n        communityId: Int? = nil,\n        postId: Int? = nil\n    ) async throws -> [ReportSnapshot] {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func getCommentReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false,\n        communityId: Int? = nil,\n        commentId: Int? = nil\n    ) async throws -> [ReportSnapshot] {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    func getMessageReports(\n        page: Int = 1,\n        limit: Int = 20,\n        unresolvedOnly: Bool = false\n    ) async throws -> [ReportSnapshot] {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func resolvePostReport(id: Int, resolved: Bool) async throws -> ReportSnapshot {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func resolveCommentReport(id: Int, resolved: Bool) async throws -> ReportSnapshot {\n        throw ApiClientError.featureUnsupported\n    }\n    \n    @discardableResult\n    func resolveMessageReport(id: Int, resolved: Bool) async throws -> ReportSnapshot {\n        throw ApiClientError.featureUnsupported\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedConnection.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-05.\n//\n\nimport Foundation\nimport Rest\n\npublic class PieFedConnection: InstanceConnection {\n    public static let softwareType: SiteSoftwareType = .pieFed\n    \n    let restClient = RestClient(errorType: ApiErrorResponse.self)\n    \n    enum PieFedConnectionError: Error {\n        case invalidSession\n    }\n    \n    struct Context {\n        let siteVersion: SiteVersion\n        let myPersonId: Int?\n    }\n\n    public let baseUrl: URL\n    public var token: String?\n    \n    private(set) var contextDataManager: SharedTaskManager<Context, (PieFedGetSiteResponse, PieFedLemmyCompatibleSiteResponse)> = .init()\n\n    public var fetchedVersion: SiteVersion? {\n        contextDataManager.fetchedValue?.siteVersion\n    }\n    \n    /// Returns the `fetchedVersion` if the version has already been fetched. Otherwise, waits until the version has been fetched before returning the received value.\n    public var version: SiteVersion {\n        get async throws {\n            try await contextDataManager.getValue().siteVersion\n        }\n    }\n    \n    public var myPersonId: Int? {\n        get async throws {\n            try await contextDataManager.getValue().myPersonId\n        }\n    }\n    \n    public var contextIsFetched: Bool {\n        contextDataManager.fetchedValue != nil\n    }\n    \n    public func ensureContextPresence() async throws {\n        try await contextDataManager.getValue()\n    }\n    \n    public required init(baseUrl: URL, token: String? = nil) {\n        self.baseUrl = baseUrl\n        self.token = token\n        contextDataManager.fetchTask = {\n            try await self.rawGetMyPerson()\n        }\n        contextDataManager.createValue = { response in\n            .init(siteVersion: .init(response.0.version), myPersonId: response.0.myUser?.localUserView.person.id)\n        }\n    }\n\n    public func updateToken(_ newToken: String) {\n        token = newToken\n    }\n\n    @discardableResult\n    func perform<Request: RestRequest>(_ request: Request, tokenOverride: String? = nil) async throws -> Request.Response {\n        let token = tokenOverride ?? token\n        do throws(RestError) {\n            return try await restClient.perform(baseUrl: baseUrl, request, token: token)\n        } catch {\n            switch error {\n            case let RestError.response(response, statusCode: _):\n                if ApiErrorResponse(error: response).isNotLoggedIn {\n                    if token == nil {\n                        throw ApiClientError.notLoggedIn\n                    } else {\n                        throw PieFedConnectionError.invalidSession\n                    }\n                } else {\n                    throw ApiClientError(from: error)\n                }\n            default:\n                throw ApiClientError(from: error)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/InstanceConnection/PiefedConnection/PieFedLemmyCompatible/PieFedLemmyCompatibleSite.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-14.\n//\n\nimport Foundation\nimport Rest\n\n// These schemas are defined by hand and only include the necessary data - parts are omitted.\n// In theory we could squeeze more data out of this by adding some of the other properties,\n// but I'd rather just wait for PieFed to implement actual support for those\n\npublic struct PieFedLemmyCompatibleGetSiteRequest: GetRequest {\n    public typealias Parameters = Int\n    public typealias Response = PieFedLemmyCompatibleSiteResponse\n    \n    public let path: String\n    public let parameters: Parameters? = nil\n    \n    init() {\n        self.path = \"api/v3/site\"\n    }\n}\n\npublic struct PieFedLemmyCompatibleSiteResponse: Codable, Hashable, Sendable {\n    public let siteView: PieFedLemmyCompatibleSiteView\n}\n\npublic extension PieFedLemmyCompatibleSiteResponse {\n    enum CodingKeys: String, CodingKey {\n        case siteView = \"site_view\"\n    }\n}\n\npublic struct PieFedLemmyCompatibleSiteView: Codable, Hashable, Sendable {\n    public let counts: LemmySiteAggregates\n}\n\npublic extension PieFedLemmyCompatibleSiteView {\n    enum CodingKeys: String, CodingKey {\n        case counts\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Protocols/APIContentAggregatesProtocol.swift",
    "content": "//\n//  ApiContentAggregatesProtocol.swift\n//  Mlem\n//\n//  Created by Sjmarf on 09/08/2023.\n//\n\nimport Foundation\n\npublic protocol ApiContentAggregatesProtocol {\n    var score: Int { get }\n    var upvotes: Int { get }\n    var downvotes: Int { get }\n    var published: Date { get }\n    var comments: Int { get }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Protocols/UpgradableProtocol.swift",
    "content": "//\n//  UpgradableProtocol.swift\n//\n//\n//  Created by Eric Andrews on 2024-05-13.\n//\n\nimport Foundation\n\n// TODO: Unified Community, Modlog remove this\npublic protocol Upgradable: Observable {\n    associatedtype Base\n    associatedtype MinimumRenderable\n    associatedtype Upgraded\n    \n    var wrappedValue: Base { get }\n    \n    func upgrade(api: ApiClient?, upgradeOperation: ((Base) async throws -> Base)?) async throws\n    func refresh(upgradeOperation: ((Base) async throws -> Base)?) async throws\n    \n    init(_ wrappedValue: Base)\n}\n\npublic extension Upgradable {\n    var isRenderable: Bool { wrappedValue is MinimumRenderable }\n    var isUpgraded: Bool { wrappedValue is Upgraded }\n    \n    func upgradeFromLocal() async throws {\n        if let wrappedValue = wrappedValue as? any Resolvable {\n            try await upgrade(\n                api: .getApiClient(url: wrappedValue.allResolvableUrls[0].removingPathComponents(), username: nil),\n                upgradeOperation: nil\n            )\n        } else {\n            assertionFailure()\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Comment/Comment1Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-07.\n//\n\nimport Foundation\n\npublic struct Comment1Snapshot: CacheIdentifiable, CommentSnapshotProviding {\n    // Won't change.\n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let creatorId: Int\n    public let postId: Int\n    public let parentCommentIds: [Int]\n    public let created: Date\n\n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Comment1!\n    public let content: String\n    public let updated: Date?\n    public let distinguished: Bool\n    public let languageId: Int\n    public let deleted: Bool\n    public let removed: Bool\n    \n    public var cacheId: Int { id }\n    \n    public init(\n        actorId: ActorIdentifier,\n        id: Int,\n        creatorId: Int,\n        postId: Int,\n        parentCommentIds: [Int],\n        created: Date,\n        content: String,\n        updated: Date?,\n        distinguished: Bool,\n        languageId: Int,\n        deleted: Bool,\n        removed: Bool\n    ) {\n        self.actorId = actorId\n        self.id = id\n        self.creatorId = creatorId\n        self.postId = postId\n        self.parentCommentIds = parentCommentIds\n        self.created = created\n        self.content = content\n        self.updated = updated\n        self.distinguished = distinguished\n        self.languageId = languageId\n        self.deleted = deleted\n        self.removed = removed\n    }\n    \n    public func merge(with snapshot: any CommentSnapshotProviding) -> any CommentSnapshotProviding {\n        if snapshot is Comment1Snapshot {\n            return self\n        }\n        if var snapshot2 = snapshot as? Comment2Snapshot {\n            snapshot2.comment = self\n            return snapshot2\n        }\n        assertionFailure(\"Unrecognized snapshot\")\n        return self\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Comment/Comment2Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-07.\n//\n\nimport Foundation\n\npublic struct Comment2Snapshot: CacheIdentifiable, CommentSnapshotProviding {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Post2.\n    public var comment: Comment1Snapshot\n    public let creator: Person1Snapshot\n    public let post: Post1Snapshot\n    public let community: Community1Snapshot\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Comment2!\n    public let commentCount: Int\n    public let creatorIsModerator: Bool\n    public let creatorIsAdmin: Bool\n    public let creatorBannedFromCommunity: Bool\n    public let votes: VotesModel\n    public let saved: Bool\n    \n    public var cacheId: Int { comment.cacheId }\n    \n    public init(\n        comment: Comment1Snapshot,\n        creator: Person1Snapshot,\n        post: Post1Snapshot,\n        community: Community1Snapshot,\n        commentCount: Int,\n        creatorIsModerator: Bool,\n        creatorIsAdmin: Bool,\n        creatorBannedFromCommunity: Bool,\n        votes: VotesModel,\n        saved: Bool\n    ) {\n        self.comment = comment\n        self.creator = creator\n        self.post = post\n        self.community = community\n        self.commentCount = commentCount\n        self.creatorIsModerator = creatorIsModerator\n        self.creatorIsAdmin = creatorIsAdmin\n        self.creatorBannedFromCommunity = creatorBannedFromCommunity\n        self.votes = votes\n        self.saved = saved\n    }\n    \n    public func merge(with snapshot: any CommentSnapshotProviding) -> any CommentSnapshotProviding {\n        self\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Comment/CommentSnapshotProviding.swift",
    "content": "//\n//  CommentSnapshotProviding.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-08-12.\n//\n\npublic protocol CommentSnapshotProviding {\n    /// Combines this snapshot with the given snapshot, returning the highest possible tier snapshot. Prefers this snapshot's values.\n    func merge(with snapshot: any CommentSnapshotProviding) -> any CommentSnapshotProviding\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Community/Community1Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-04.\n//\n\nimport Foundation\n\npublic struct Community1Snapshot: CacheIdentifiable {\n    // Won't change.\n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let name: String\n    public let created: Date\n    public let instanceId: Int\n\n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Community1!\n    public let updated: Date?\n    public let displayName: String\n    public let description: String?\n    public let deleted: Bool\n    public let removed: Bool\n    public let nsfw: Bool\n    public let avatar: URL?\n    public let banner: URL?\n    public let hidden: Bool\n    public let onlyModeratorsCanPost: Bool\n    \n    // This is a dodgy workaround for https://codeberg.org/rimu/pyfedi/issues/882\n    // TODO: If that issue gets fixed, we can remove this\n    public let allPropertiesPresent: Bool\n\n    public var cacheId: Int { id }\n    \n    public init(\n        actorId: ActorIdentifier,\n        id: Int,\n        name: String,\n        created: Date,\n        instanceId: Int,\n        updated: Date?,\n        displayName: String,\n        description: String?,\n        deleted: Bool,\n        removed: Bool,\n        nsfw: Bool,\n        avatar: URL?,\n        banner: URL?,\n        hidden: Bool,\n        onlyModeratorsCanPost: Bool,\n        allPropertiesPresent: Bool\n    ) {\n        self.actorId = actorId\n        self.id = id\n        self.name = name\n        self.created = created\n        self.instanceId = instanceId\n        self.updated = updated\n        self.displayName = displayName\n        self.description = description\n        self.deleted = deleted\n        self.removed = removed\n        self.nsfw = nsfw\n        self.avatar = avatar\n        self.banner = banner\n        self.hidden = hidden\n        self.onlyModeratorsCanPost = onlyModeratorsCanPost\n        self.allPropertiesPresent = allPropertiesPresent\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Community/Community2Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-04.\n//\n\nimport Foundation\n\npublic struct Community2Snapshot: CacheIdentifiable {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Community2.\n    public let community: Community1Snapshot\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Community2!\n    public let subscription: SubscriptionModel\n    public let postCount: Int\n    public let commentCount: Int\n    public let activeUserCount: ActiveUserCount\n    public let bannedFromCommunity: Bool?\n    \n    public var cacheId: Int { community.cacheId }\n    \n    public init(\n        community: Community1Snapshot,\n        subscription: SubscriptionModel,\n        postCount: Int,\n        commentCount: Int,\n        activeUserCount: ActiveUserCount,\n        bannedFromCommunity: Bool?\n    ) {\n        self.community = community\n        self.subscription = subscription\n        self.postCount = postCount\n        self.commentCount = commentCount\n        self.activeUserCount = activeUserCount\n        self.bannedFromCommunity = bannedFromCommunity\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Community/Community3Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-05.\n//\n\nimport Foundation\n\npublic struct Community3Snapshot: CacheIdentifiable {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Community3.\n    public let community: Community2Snapshot\n    \n    public let instance: Instance1Snapshot?\n    public let moderators: [Person1Snapshot]\n    public let discussionLanguageIds: Set<Int>\n    \n    public var cacheId: Int { community.cacheId }\n    \n    public init(\n        community: Community2Snapshot,\n        instance: Instance1Snapshot?,\n        moderators: [Person1Snapshot],\n        discussionLanguageIds: Set<Int>\n    ) {\n        self.community = community\n        self.instance = instance\n        self.moderators = moderators\n        self.discussionLanguageIds = discussionLanguageIds\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/ImageUpload/ImageUpload1Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-07-05.\n//\n\nimport Foundation\n\npublic struct ImageUpload1Snapshot: CacheIdentifiable {\n    public let url: URL\n    \n    public let alias: String?\n    public let deleteToken: String?\n    \n    public init(\n        url: URL,\n        alias: String?,\n        deleteToken: String?\n    ) {\n        self.url = url\n        self.alias = alias\n        self.deleteToken = deleteToken\n    }\n    \n    public var cacheId: Int {\n        url.hashValue\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Instance/Instance1Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-11.\n//\n\nimport Foundation\n\npublic struct Instance1Snapshot: CacheIdentifiable {\n    // Won't change.\n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let instanceId: Int\n    public let created: Date\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Instance1!\n    public let updated: Date?\n    public let publicKey: String\n    public var displayName: String\n    public var description: String?\n    public var shortDescription: String?\n    public var avatar: URL?\n    public var banner: URL?\n    public var lastRefresh: Date\n    public var contentWarning: String?\n    \n    public var cacheId: Int { id }\n    \n    public init(\n        actorId: ActorIdentifier,\n        id: Int,\n        instanceId: Int,\n        created: Date,\n        updated: Date?,\n        publicKey: String,\n        displayName: String,\n        description: String? = nil,\n        shortDescription: String? = nil,\n        avatar: URL? = nil,\n        banner: URL? = nil,\n        lastRefresh: Date,\n        contentWarning: String? = nil\n    ) {\n        self.actorId = actorId\n        self.id = id\n        self.instanceId = instanceId\n        self.created = created\n        self.updated = updated\n        self.publicKey = publicKey\n        self.displayName = displayName\n        self.description = description\n        self.shortDescription = shortDescription\n        self.avatar = avatar\n        self.banner = banner\n        self.lastRefresh = lastRefresh\n        self.contentWarning = contentWarning\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Instance/Instance2Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-11.\n//\n\nimport Foundation\n\npublic struct Instance2Snapshot: CacheIdentifiable {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Instance2.\n    public let instance: Instance1Snapshot\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Post2!\n    public var setup: Bool\n    public var voteFederationMode: VoteFederationMode\n    public var nsfwContentEnabled: Bool\n    public var communityCreationRestrictedToAdmins: Bool\n    public var emailVerificationRequired: Bool\n    public var applicationQuestion: String?\n    public var isPrivate: Bool\n    public var defaultTheme: String\n    public var defaultFeed: ListingType\n    public var legalInformation: String?\n    public var hideModlogNames: Bool\n    public var emailApplicationsToAdmins: Bool\n    public var emailReportsToAdmins: Bool\n    public var slurFilterRegex: String?\n    public var actorNameMaxLength: Int\n    public var federationEnabled: Bool\n    public var captchaEnabled: Bool\n    public var captchaDifficulty: CaptchaDifficulty?\n    public var registrationMode: RegistrationMode\n    public var federationSignedFetch: Bool?\n    public var defaultPostListingMode: PostFeedViewMode?\n    public var defaultPostSortType: PostSortType?\n    public var userCount: Int\n    public var postCount: Int\n    public var commentCount: Int\n    public var communityCount: Int\n    public var activeUserCount: ActiveUserCount\n    \n    public var cacheId: Int { instance.cacheId }\n    \n    public init(\n        instance: Instance1Snapshot,\n        setup: Bool,\n        voteFederationMode: VoteFederationMode,\n        nsfwContentEnabled: Bool,\n        communityCreationRestrictedToAdmins: Bool,\n        emailVerificationRequired: Bool,\n        applicationQuestion: String? = nil,\n        isPrivate: Bool,\n        defaultTheme: String,\n        defaultFeed: ListingType,\n        legalInformation: String? = nil,\n        hideModlogNames: Bool,\n        emailApplicationsToAdmins: Bool,\n        emailReportsToAdmins: Bool,\n        slurFilterRegex: String? = nil,\n        actorNameMaxLength: Int,\n        federationEnabled: Bool,\n        captchaEnabled: Bool,\n        captchaDifficulty: CaptchaDifficulty? = nil,\n        registrationMode: RegistrationMode,\n        federationSignedFetch: Bool? = nil,\n        defaultPostListingMode: PostFeedViewMode? = nil,\n        defaultPostSortType: PostSortType? = nil,\n        userCount: Int,\n        postCount: Int,\n        commentCount: Int,\n        communityCount: Int,\n        activeUserCount: ActiveUserCount\n    ) {\n        self.instance = instance\n        self.setup = setup\n        self.voteFederationMode = voteFederationMode\n        self.nsfwContentEnabled = nsfwContentEnabled\n        self.communityCreationRestrictedToAdmins = communityCreationRestrictedToAdmins\n        self.emailVerificationRequired = emailVerificationRequired\n        self.applicationQuestion = applicationQuestion\n        self.isPrivate = isPrivate\n        self.defaultTheme = defaultTheme\n        self.defaultFeed = defaultFeed\n        self.legalInformation = legalInformation\n        self.hideModlogNames = hideModlogNames\n        self.emailApplicationsToAdmins = emailApplicationsToAdmins\n        self.emailReportsToAdmins = emailReportsToAdmins\n        self.slurFilterRegex = slurFilterRegex\n        self.actorNameMaxLength = actorNameMaxLength\n        self.federationEnabled = federationEnabled\n        self.captchaEnabled = captchaEnabled\n        self.captchaDifficulty = captchaDifficulty\n        self.registrationMode = registrationMode\n        self.federationSignedFetch = federationSignedFetch\n        self.defaultPostListingMode = defaultPostListingMode\n        self.defaultPostSortType = defaultPostSortType\n        self.userCount = userCount\n        self.postCount = postCount\n        self.commentCount = commentCount\n        self.communityCount = communityCount\n        self.activeUserCount = activeUserCount\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Instance/Instance3Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-11.\n//\n\nimport Foundation\n\npublic struct Instance3Snapshot: CacheIdentifiable {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Instance3.\n    public let instance: Instance2Snapshot\n    \n    // Won't Change.\n    public let allLanguages: [Locale.Language]\n\n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Instance3!\n    public let software: SiteSoftware\n    // This excludes the \"undetermined\" language identifier (which is 0),\n    // because its presence or absence doesn't actually affect whether you're\n    // able to create a post with \"undetermined\" as the language\n    public var allowedLanguageIds: Set<Int>\n    public let blockedUrls: [InstanceUrlBlockRecord]?\n    public let administrators: [Person2Snapshot]\n\n    public var cacheId: Int { instance.cacheId }\n    \n    public init(\n        instance: Instance2Snapshot,\n        allLanguages: [Locale.Language],\n        software: SiteSoftware,\n        allowedLanguageIds: Set<Int>,\n        blockedUrls: [InstanceUrlBlockRecord]?,\n        administrators: [Person2Snapshot]\n    ) {\n        self.instance = instance\n        self.allLanguages = allLanguages\n        self.software = software\n        self.allowedLanguageIds = allowedLanguageIds\n        self.blockedUrls = blockedUrls\n        self.administrators = administrators\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Message/Message1Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-10.\n//\n\nimport Foundation\n\npublic struct Message1Snapshot: CacheIdentifiable {\n    // Won't change.\n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let creatorId: Int\n    public let recipientId: Int\n    public let created: Date\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Message1!\n    public let content: String\n    public let updated: Date?\n    public let read: Bool\n    public let deleted: Bool\n    \n    public var cacheId: Int { id }\n    \n    public init(\n        actorId: ActorIdentifier,\n        id: Int,\n        creatorId: Int,\n        recipientId: Int,\n        created: Date,\n        content: String,\n        updated: Date?,\n        read: Bool,\n        deleted: Bool\n    ) {\n        self.actorId = actorId\n        self.id = id\n        self.creatorId = creatorId\n        self.recipientId = recipientId\n        self.created = created\n        self.content = content\n        self.updated = updated\n        self.read = read\n        self.deleted = deleted\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Message/Message2Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-10.\n//\n\nimport Foundation\n\npublic struct Message2Snapshot: CacheIdentifiable {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Message2.\n    public let message: Message1Snapshot\n    public let creator: Person1Snapshot\n    public let recipient: Person1Snapshot\n    \n    public var cacheId: Int { message.cacheId }\n    \n    public init(\n        message: Message1Snapshot,\n        creator: Person1Snapshot,\n        recipient: Person1Snapshot\n    ) {\n        self.message = message\n        self.creator = creator\n        self.recipient = recipient\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/ModlogEntry/ModlogEntryContentSnapshot.swift",
    "content": "//\n//  ModlogEntryContentSnapshot.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-13.\n//\n\nimport Foundation\n\npublic enum ModlogEntryContentSnapshot {\n    case removePost(\n        _ post: Post1Snapshot,\n        community: Community1Snapshot,\n        removed: Bool,\n        reason: String?\n    )\n    case lockPost(\n        _ post: Post1Snapshot,\n        community: Community1Snapshot,\n        locked: Bool\n    )\n    case pinPost(\n        _ post: Post1Snapshot,\n        community: Community1Snapshot,\n        pinned: Bool,\n        type: PostFeatureType\n    )\n    case purgePost(reason: String?)\n    \n    case removeComment(\n        _ comment: Comment1Snapshot,\n        creator: Person1Snapshot,\n        post: Post1Snapshot,\n        community: Community1Snapshot,\n        removed: Bool,\n        reason: String?\n    )\n    case purgeComment(reason: String?)\n    \n    case removeCommunity(\n        _ community: Community1Snapshot,\n        removed: Bool,\n        reason: String?\n    )\n    case purgeCommunity(reason: String?)\n    \n    case hideCommunity(\n        _ community: Community1Snapshot,\n        hidden: Bool,\n        reason: String?\n    )\n    case transferCommunityOwnership(\n        person: Person1Snapshot,\n        community: Community1Snapshot\n    )\n    \n    case updatePersonModeratorStatus(\n        person: Person1Snapshot,\n        community: Community1Snapshot,\n        appointed: Bool\n    )\n    case updatePersonAdminStatus(\n        person: Person1Snapshot,\n        appointed: Bool\n    )\n    case banPersonFromCommunity(\n        person: Person1Snapshot,\n        community: Community1Snapshot,\n        banned: Bool,\n        reason: String?,\n        expires: Date?\n    )\n    case banPersonFromInstance(\n        person: Person1Snapshot,\n        banned: Bool,\n        reason: String?,\n        expires: Date?\n    )\n    case purgePerson(reason: String?)\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/ModlogEntry/ModlogEntrySnapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-13.\n//\n\nimport Foundation\n\npublic struct ModlogEntrySnapshot {\n    public let created: Date\n    public let moderator: Person1Snapshot?\n    public let type: ModlogEntryContentSnapshot\n    \n    public init(created: Date, moderator: Person1Snapshot?, type: ModlogEntryContentSnapshot) {\n        self.created = created\n        self.moderator = moderator\n        self.type = type\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Notification/InboxNotificationSnapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-06.\n//\n\nimport Foundation\n\npublic struct InboxNotificationSnapshot: CacheIdentifiable {\n    public let id: Int\n    public let contentId: Int\n    public var read: Bool\n    public var content: InboxNotificationContentSnapshot\n\n    public var cacheId: Int { id }\n\n    public init(\n        id: Int,\n        contentId: Int,\n        read: Bool,\n        content: InboxNotificationContentSnapshot\n    ) {\n        self.id = id\n        self.contentId = contentId\n        self.read = read\n        self.content = content\n    }\n}\n\npublic enum InboxNotificationContentSnapshot {\n    case reply(Comment2Snapshot)\n    case mention(Comment2Snapshot)\n    case message(Message2Snapshot)\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Person/Person1Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-04.\n//\n\nimport Foundation\n\npublic struct Person1Snapshot: CacheIdentifiable {\n    // Won't change.\n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let name: String\n    public let created: Date\n    public let instanceId: Int\n\n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Person1!\n    public let displayName: String\n    public let avatar: URL?\n    public let banner: URL?\n    public let note: String?\n    public let updated: Date?\n    public let description: String?\n    public let matrixUserId: String?\n    public let isBot: Bool\n    public let instanceBan: InstanceBanType\n    public let deleted: Bool\n    \n    // This is a dodgy workaround for https://codeberg.org/rimu/pyfedi/issues/882\n    // TODO: If that issue gets fixed, we can remove this\n    public let allPropertiesPresent: Bool\n\n    public var cacheId: Int { id }\n    \n    public init(\n        actorId: ActorIdentifier,\n        id: Int,\n        name: String,\n        created: Date,\n        instanceId: Int,\n        displayName: String,\n        avatar: URL?,\n        banner: URL?,\n        note: String?,\n        updated: Date?,\n        description: String?,\n        matrixUserId: String?,\n        isBot: Bool,\n        instanceBan: InstanceBanType,\n        deleted: Bool,\n        allPropertiesPresent: Bool\n    ) {\n        self.actorId = actorId\n        self.id = id\n        self.name = name\n        self.created = created\n        self.instanceId = instanceId\n        self.displayName = displayName\n        self.avatar = avatar\n        self.banner = banner\n        self.note = note\n        self.updated = updated\n        self.description = description\n        self.matrixUserId = matrixUserId\n        self.isBot = isBot\n        self.instanceBan = instanceBan\n        self.deleted = deleted\n        self.allPropertiesPresent = allPropertiesPresent\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Person/Person2Snapshot.swift",
    "content": "//\n//  Person2ApiBacker.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-02-19.\n//\n\nimport Foundation\n\npublic struct Person2Snapshot: CacheIdentifiable {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Person2.\n    public let person: Person1Snapshot\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Person2!\n    public let isAdmin: Bool\n    public let postCount: Int\n    public let commentCount: Int\n    \n    public var cacheId: Int { person.cacheId }\n    \n    public init(\n        person: Person1Snapshot,\n        isAdmin: Bool,\n        postCount: Int,\n        commentCount: Int\n    ) {\n        self.person = person\n        self.isAdmin = isAdmin\n        self.postCount = postCount\n        self.commentCount = commentCount\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Person/Person3Snapshot.swift",
    "content": "//\n//  Person3ApiBacker.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-03-01.\n//\n\nimport Foundation\n\npublic struct Person3Snapshot: CacheIdentifiable {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Person3.\n    let person: Person2Snapshot\n    let site: Instance1Snapshot?\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Person3!\n    let moderatedCommunities: [Community1Snapshot]\n    \n    public var cacheId: Int { person.cacheId }\n    \n    public init(\n        person: Person2Snapshot,\n        site: Instance1Snapshot?,\n        moderatedCommunities: [Community1Snapshot]\n    ) {\n        self.person = person\n        self.site = site\n        self.moderatedCommunities = moderatedCommunities\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Person/Person4Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-04.\n//\n\nimport Foundation\n\npublic struct Person4Snapshot: CacheIdentifiable {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Person2.\n    public let person: Person3Snapshot\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Person3!\n    public var email: String?\n    public var showNsfw: Bool\n    public var theme: String\n    public var defaultListingType: ListingType\n    public var interfaceLanguage: String\n    public var showAvatars: Bool\n    public var sendNotificationsToEmail: Bool\n    public var showScores: Bool\n    public var showBotAccounts: Bool\n    public var showReadPosts: Bool\n    public var discussionLanguageIds: Set<Int>\n    public var emailVerified: Bool\n    public var acceptedApplication: Bool\n    public var openLinksInNewTab: Bool?\n    public var blurNsfw: Bool?\n    public var autoExpandImages: Bool?\n    public var infiniteScrollEnabled: Bool?\n    public var postListingMode: PostFeedViewMode?\n    public var totp2faEnabled: Bool?\n    public var enableKeyboardNavigation: Bool?\n    public var enableAnimatedImages: Bool?\n    public var collapseBotComments: Bool?\n\n    public var cacheId: Int { person.cacheId }\n    \n    public init(\n        person: Person3Snapshot,\n        email: String? = nil,\n        showNsfw: Bool,\n        theme: String,\n        defaultListingType: ListingType,\n        interfaceLanguage: String,\n        showAvatars: Bool,\n        sendNotificationsToEmail: Bool,\n        showScores: Bool,\n        showBotAccounts: Bool,\n        showReadPosts: Bool,\n        discussionLanguageIds: Set<Int>,\n        emailVerified: Bool,\n        acceptedApplication: Bool,\n        openLinksInNewTab: Bool? = nil,\n        blurNsfw: Bool? = nil,\n        autoExpandImages: Bool? = nil,\n        infiniteScrollEnabled: Bool? = nil,\n        postListingMode: PostFeedViewMode? = nil,\n        totp2faEnabled: Bool? = nil,\n        enableKeyboardNavigation: Bool? = nil,\n        enableAnimatedImages: Bool? = nil,\n        collapseBotComments: Bool? = nil\n    ) {\n        self.person = person\n        self.email = email\n        self.showNsfw = showNsfw\n        self.theme = theme\n        self.defaultListingType = defaultListingType\n        self.interfaceLanguage = interfaceLanguage\n        self.showAvatars = showAvatars\n        self.sendNotificationsToEmail = sendNotificationsToEmail\n        self.showScores = showScores\n        self.showBotAccounts = showBotAccounts\n        self.showReadPosts = showReadPosts\n        self.discussionLanguageIds = discussionLanguageIds\n        self.emailVerified = emailVerified\n        self.acceptedApplication = acceptedApplication\n        self.openLinksInNewTab = openLinksInNewTab\n        self.blurNsfw = blurNsfw\n        self.autoExpandImages = autoExpandImages\n        self.infiniteScrollEnabled = infiniteScrollEnabled\n        self.postListingMode = postListingMode\n        self.totp2faEnabled = totp2faEnabled\n        self.enableKeyboardNavigation = enableKeyboardNavigation\n        self.enableAnimatedImages = enableAnimatedImages\n        self.collapseBotComments = collapseBotComments\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/PersonVote/PersonVoteSnapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-10.\n//\n\nimport Foundation\n\npublic struct PersonVoteSnapshot: CacheIdentifiable {\n    public let creator: Person1Snapshot\n    public let score: Int\n    public let creatorBannedFromCommunity: Bool?\n    \n    public var cacheId: Int { creator.id }\n    \n    public init(\n        creator: Person1Snapshot,\n        score: Int,\n        creatorBannedFromCommunity: Bool?\n    ) {\n        self.creator = creator\n        self.score = score\n        self.creatorBannedFromCommunity = creatorBannedFromCommunity\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Post/Post1Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-06.\n//\n\nimport Foundation\n\npublic struct Post1Snapshot: CacheIdentifiable, PostSnapshotProviding {\n    // Won't change.\n    public let actorId: ActorIdentifier\n    public let id: Int\n    public let creatorId: Int\n    public let communityId: Int\n    public let created: Date\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Post1!\n    public let title: String\n    public let content: String?\n    public let linkUrl: URL?\n    public let embed: PostEmbed?\n    public let poll: PostPoll?\n    public let nsfw: Bool\n    public let thumbnailUrl: URL?\n    public let updated: Date?\n    public let languageId: Int\n    public let altText: String?\n    public let deleted: Bool\n    public let removed: Bool\n    public let pinnedCommunity: Bool\n    public let pinnedInstance: Bool\n    public let locked: Bool\n\n    public var cacheId: Int { id }\n    \n    public init(\n        actorId: ActorIdentifier,\n        id: Int,\n        creatorId: Int,\n        communityId: Int,\n        created: Date,\n        title: String,\n        content: String?,\n        linkUrl: URL?,\n        embed: PostEmbed?,\n        poll: PostPoll?,\n        nsfw: Bool,\n        thumbnailUrl: URL?,\n        updated: Date?,\n        languageId: Int,\n        altText: String?,\n        deleted: Bool,\n        removed: Bool,\n        pinnedCommunity: Bool,\n        pinnedInstance: Bool,\n        locked: Bool\n    ) {\n        self.actorId = actorId\n        self.id = id\n        self.creatorId = creatorId\n        self.communityId = communityId\n        self.created = created\n        self.title = title\n        self.content = content\n        self.linkUrl = linkUrl\n        self.embed = embed\n        self.poll = poll\n        self.nsfw = nsfw\n        self.thumbnailUrl = thumbnailUrl\n        self.updated = updated\n        self.languageId = languageId\n        self.altText = altText\n        self.deleted = deleted\n        self.removed = removed\n        self.pinnedCommunity = pinnedCommunity\n        self.pinnedInstance = pinnedInstance\n        self.locked = locked\n    }\n    \n    public func merge(with snapshot: any PostSnapshotProviding) -> any PostSnapshotProviding {\n        if snapshot is Post1Snapshot {\n            return self\n        }\n        if var snapshot2 = snapshot as? Post2Snapshot {\n            snapshot2.post = self\n            return snapshot2\n        }\n        if var snapshot3 = snapshot as? Post3Snapshot {\n            snapshot3.post.post = self\n            return snapshot3\n        }\n        assertionFailure(\"Unrecognized snapshot\")\n        return self\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Post/Post2Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-06.\n//\n\nimport Foundation\n\npublic struct Post2Snapshot: CacheIdentifiable, PostSnapshotProviding {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Post2.\n    public var post: Post1Snapshot\n    public let creator: Person1Snapshot\n    public let community: Community1Snapshot\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Post2!\n    public let commentCount: Int\n    public let unreadCommentCount: Int\n    public let creatorIsModerator: Bool\n    public let creatorIsAdmin: Bool\n    public let creatorBannedFromCommunity: Bool\n    public let creatorBlocked: Bool\n    public let votes: VotesModel\n    public let saved: Bool\n    public var read: Bool\n    public var hidden: Bool\n    \n    public var cacheId: Int { post.cacheId }\n    public var actorId: ActorIdentifier { post.actorId }\n    \n    public init(\n        post: Post1Snapshot,\n        creator: Person1Snapshot,\n        community: Community1Snapshot,\n        commentCount: Int,\n        unreadCommentCount: Int,\n        creatorIsModerator: Bool,\n        creatorIsAdmin: Bool,\n        creatorBannedFromCommunity: Bool,\n        creatorBlocked: Bool,\n        votes: VotesModel,\n        saved: Bool,\n        read: Bool,\n        hidden: Bool\n    ) {\n        self.post = post\n        self.creator = creator\n        self.community = community\n        self.commentCount = commentCount\n        self.unreadCommentCount = unreadCommentCount\n        self.creatorIsModerator = creatorIsModerator\n        self.creatorIsAdmin = creatorIsAdmin\n        self.creatorBannedFromCommunity = creatorBannedFromCommunity\n        self.creatorBlocked = creatorBlocked\n        self.votes = votes\n        self.saved = saved\n        self.read = read\n        self.hidden = hidden\n    }\n    \n    public func merge(with snapshot: any PostSnapshotProviding) -> any PostSnapshotProviding {\n        if snapshot is Post1Snapshot || snapshot is Post2Snapshot {\n            return self\n        }\n        if var snapshot3 = snapshot as? Post3Snapshot {\n            snapshot3.post = self\n            return snapshot3\n        }\n        assertionFailure(\"Unrecognized snapshot\")\n        return self\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Post/Post3Snapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-07.\n//\n\nimport Foundation\n\npublic struct Post3Snapshot: CacheIdentifiable, PostSnapshotProviding {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Post2.\n    public var post: Post2Snapshot\n    public let community: Community2Snapshot\n    \n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Post3!\n    public let crossPosts: [Post2Snapshot]\n    \n    public var cacheId: Int { post.cacheId }\n    public var actorId: ActorIdentifier { post.actorId }\n    \n    public init(\n        post: Post2Snapshot,\n        community: Community2Snapshot,\n        crossPosts: [Post2Snapshot]\n    ) {\n        self.post = post\n        self.community = community\n        self.crossPosts = crossPosts\n    }\n    \n    public func merge(with snapshot: any PostSnapshotProviding) -> any PostSnapshotProviding {\n        self\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Post/PostSnapshotProviding.swift",
    "content": "//\n//  PostSnapshotProviding.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-07-04.\n//\n\npublic protocol PostSnapshotProviding: CacheIdentifiable, ActorIdentifiable {\n    /// Combines this snapshot with the given snapshot, returning the highest possible tier snapshot. Prefers this snapshot's values.\n    func merge(with snapshot: any PostSnapshotProviding) -> any PostSnapshotProviding\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/ProfileDetails.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-09-07.\n//\n\nimport Foundation\n\npublic struct ProfileDetails: Hashable, Sendable {\n    public var avatar: URL?\n    public var banner: URL?\n    public var displayName: String?\n    public var description: String?\n    public var matrixUserId: String?\n}\n\npublic struct ProfileDetailsMutation {\n    let originalDetails: ProfileDetails\n    let newDetails: ProfileDetails\n\n    func isValid(forSoftware software: SiteSoftware) -> Bool {\n        if originalDetails.displayName != newDetails.displayName, !software.supports(.editDisplayName) { return false }\n        return true\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/RegistrationApplication/RegistrationApplicationSnapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-10.\n//\n\nimport Foundation\n\npublic struct RegistrationApplicationSnapshot: CacheIdentifiable {\n    // Won't change.\n    public let id: Int\n    public let created: Date\n    \n    // I don't *think* these can change, but I'm assuming they do\n    // incase the ability to edit applications is added in future.\n    // Update RegistrationApplication if you change these!\n    public let questionResponse: String\n    public let email: String?\n    public let showNsfw: Bool\n    public let creator: Person1Snapshot\n\n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of RegistrationApplication!\n    public let emailVerified: Bool\n    public let resolver: Person1Snapshot?\n    public let resolution: RegistrationApplication.ResolutionState\n    \n    public var cacheId: Int { id }\n    \n    public init(\n        id: Int,\n        created: Date,\n        questionResponse: String,\n        email: String?,\n        showNsfw: Bool,\n        creator: Person1Snapshot,\n        emailVerified: Bool,\n        resolver: Person1Snapshot?,\n        resolution: RegistrationApplication.ResolutionState\n    ) {\n        self.id = id\n        self.created = created\n        self.questionResponse = questionResponse\n        self.email = email\n        self.showNsfw = showNsfw\n        self.creator = creator\n        self.emailVerified = emailVerified\n        self.resolver = resolver\n        self.resolution = resolution\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/Snapshot/Report/ReportSnapshot.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-10.\n//\n\nimport Foundation\n\npublic struct ReportSnapshot: CacheIdentifiable {\n    // Won't change, but the corresponding models need to\n    // be updated within the `update` method of Report.\n    public let creator: Person1Snapshot\n    \n    // Won't change.\n    public let id: Int\n    public let created: Date\n\n    // May change. If you add/remove items from this list,\n    // remember to also amend the `update` method of Report!\n    public let resolver: Person1Snapshot?\n    public let updated: Date?\n    public let resolved: Bool\n    public let reason: String\n    \n    public let target: ReportTargetSnapshot\n    \n    public var cacheId: Int {\n        var hasher = Hasher()\n        hasher.combine(target.type)\n        hasher.combine(id)\n        return hasher.finalize()\n    }\n    \n    public init(\n        creator: Person1Snapshot,\n        id: Int,\n        created: Date,\n        resolver: Person1Snapshot?,\n        updated: Date?,\n        resolved: Bool,\n        reason: String,\n        target: ReportTargetSnapshot\n    ) {\n        self.creator = creator\n        self.id = id\n        self.created = created\n        self.resolver = resolver\n        self.updated = updated\n        self.resolved = resolved\n        self.reason = reason\n        self.target = target\n    }\n}\n\npublic enum ReportTargetSnapshot {\n    case post(Post2Snapshot)\n    case comment(Comment2Snapshot)\n    case message(Message2Snapshot)\n    \n    var type: ReportType {\n        switch self {\n        case .post: .post\n        case .comment: .comment\n        case .message: .message\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Sources/MlemMiddleware/UsernameValidity.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-05-24.\n//\n\nimport Foundation\n\npublic enum UsernameValidity: Hashable, Sendable {\n    case available\n    case taken\n    case invalid(InvalidityReason)\n    \n    public enum InvalidityReason: Hashable, Sendable {\n        case tooShort(minLength: Int)\n        case tooLong(maxLength: Int)\n        case containsInvalidCharacters(_ characters: Set<Character>)\n        case other\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/MlemMiddleware/Tests/MlemMiddlewareTests/MlemMiddlewareTests.swift",
    "content": "@testable import MlemMiddleware\nimport XCTest\n\nfinal class MlemMiddlewareTests: XCTestCase {\n    func testExample() throws {\n        // XCTest Documentation\n        // https://developer.apple.com/documentation/xctest\n\n        // Defining Test Cases and Test Methods\n        // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/.gitignore",
    "content": ".DS_Store\n/.build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Package.resolved",
    "content": "{\n  \"originHash\" : \"433eb46e437fba071b47444bcf712c57872e1973de4812d146c7db18e50834e2\",\n  \"pins\" : [\n    {\n      \"identity\" : \"libwebp-xcode\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/libwebp-Xcode.git\",\n      \"state\" : {\n        \"revision\" : \"0d60654eeefd5d7d2bef3835804892c40225e8b2\",\n        \"version\" : \"1.5.0\"\n      }\n    },\n    {\n      \"identity\" : \"nuke\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/kean/Nuke.git\",\n      \"state\" : {\n        \"revision\" : \"0ead44350d2737db384908569c012fe67c421e4d\",\n        \"version\" : \"12.8.0\"\n      }\n    },\n    {\n      \"identity\" : \"sdwebimage\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/SDWebImage.git\",\n      \"state\" : {\n        \"revision\" : \"cac9a55a3ae92478a2c95042dcc8d9695d2129ca\",\n        \"version\" : \"5.21.0\"\n      }\n    },\n    {\n      \"identity\" : \"sdwebimagewebpcoder\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/SDWebImageWebPCoder\",\n      \"state\" : {\n        \"revision\" : \"f534cfe830a7807ecc3d0332127a502426cfa067\",\n        \"version\" : \"0.14.6\"\n      }\n    }\n  ],\n  \"version\" : 3\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"QuickSwipes\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"QuickSwipes\",\n            targets: [\"QuickSwipes\"]\n        )\n    ],\n    dependencies: [\n        .package(path: \"../Theming\"),\n        .package(path: \"../ComponentViews\"),\n        .package(path: \"../Icons\"),\n        .package(path: \"../Haptics\"),\n        .package(path: \"../MlemLogger\")\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"QuickSwipes\",\n            dependencies: [\n                .byName(name: \"Theming\"),\n                .byName(name: \"ComponentViews\"),\n                .byName(name: \"Icons\"),\n                .byName(name: \"Haptics\"),\n                .byName(name: \"MlemLogger\")\n            ],\n            swiftSettings: [\n                .swiftLanguageMode(.v5),\n                .enableUpcomingFeature(\"BareSlashRegexLiterals\")\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Sources/QuickSwipes/Array+Extensions.swift",
    "content": "//\n//  File.swift\n//  QuickSwipes\n//\n//  Created by Sjmarf on 2025-08-23.\n//\n\nimport Foundation\n\nextension Array {\n    subscript(safeIndex index: Int) -> Element? {\n        guard index >= 0, index < endIndex else {\n            return nil\n        }\n        return self[index]\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Sources/QuickSwipes/EnvironmentValues+Extensions.swift",
    "content": "//\n//  File.swift\n//  QuickSwipes\n//\n//  Created by Sjmarf on 2025-08-22.\n//\n\nimport SwiftUI\n\nextension EnvironmentValues {\n    @Entry var quickSwipeThresholdSet: QuickSwipeThresholdSet = .default\n    @Entry var quickSwipeMinimumDrag: CGFloat = 20\n    @Entry var quickSwipeIconSize: CGFloat = 16\n    @Entry var quickSwipeCornerRadius: CGFloat = 28\n    @Entry var quickSwipesEnabled: Bool = true\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Sources/QuickSwipes/PanGesture.swift",
    "content": "//\n//  PanGesture.swift\n//  Mlem\n//\n//  Created by Sjmarf on 17/09/2024.\n//\n\nimport SwiftUI\n\nstruct PanGesture: UIGestureRecognizerRepresentable {\n    /// If provided, the gesture will not register within `leadingBuffer` px of the leading edge\n    let leadingBuffer: CGFloat?\n    var handle: (UIPanGestureRecognizer) -> Void\n    \n    func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator { .init(leadingBuffer: leadingBuffer) }\n    \n    func makeUIGestureRecognizer(context: Context) -> UIPanGestureRecognizer {\n        let gesture = UIPanGestureRecognizer()\n        gesture.delegate = context.coordinator\n        gesture.isEnabled = true\n        return gesture\n    }\n    \n    func handleUIGestureRecognizerAction(_ recognizer: UIPanGestureRecognizer, context: Context) {\n        handle(recognizer)\n    }\n    \n    class Coordinator: NSObject, UIGestureRecognizerDelegate {\n        let leadingBuffer: CGFloat\n        \n        init(leadingBuffer: CGFloat?) {\n            self.leadingBuffer = leadingBuffer ?? 0\n        }\n        \n        func gestureRecognizer(\n            _ gestureRecognizer: UIGestureRecognizer,\n            shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer\n        ) -> Bool {\n            false\n        }\n        \n        func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {\n            guard let panRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { return false }\n            \n            // prevent swipe from interfering with interactive swipe back\n            guard panRecognizer.location(in: gestureRecognizer.view).x >= leadingBuffer else { return false }\n\n            let velocity = panRecognizer.velocity(in: gestureRecognizer.view)\n            return abs(velocity.y) < abs(velocity.x)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Sources/QuickSwipes/QuickSwipeAction.swift",
    "content": "//\n//  File.swift\n//  QuickSwipes\n//\n//  Created by Sjmarf on 2025-08-22.\n//\n\nimport Foundation\nimport Icons\nimport Theming\n\npublic struct QuickSwipeAction {\n    enum ActionType {\n        case callback(@MainActor () -> Void, confirmationPrompt: String?)\n        case choice(QuickSwipeChoiceGroup)\n    }\n    \n    var enabled: Bool\n    var perform: ActionType\n    \n    var color: ThemedColor\n    var icon: Icon\n    \n    public init(\n        icon: Icon,\n        color: ThemedColor,\n        enabled: Bool,\n        confirmationPrompt: String?,\n        callback: @MainActor @escaping () -> Void\n    ) {\n        self.icon = icon\n        self.color = color\n        self.enabled = enabled\n        self.perform = .callback(callback, confirmationPrompt: confirmationPrompt)\n    }\n    \n    public init(\n        icon: Icon,\n        color: ThemedColor,\n        enabled: Bool,\n        alertTitle: LocalizedStringResource,\n        choices: [QuickSwipeChoice]\n    ) {\n        self.icon = icon\n        self.color = color\n        self.enabled = enabled\n        self.perform = .choice(.init(title: .init(localized: alertTitle), items: choices))\n    }\n    \n    @_disfavoredOverload\n    public init(\n        icon: Icon,\n        color: ThemedColor,\n        enabled: Bool,\n        alertTitle: String,\n        choices: [QuickSwipeChoice]\n    ) {\n        self.icon = icon\n        self.color = color\n        self.enabled = enabled\n        self.perform = .choice(.init(title: alertTitle, items: choices))\n    }\n}\n\nstruct QuickSwipeChoiceGroup {\n    let title: String\n    let items: [QuickSwipeChoice]\n}\n\npublic struct QuickSwipeChoice {\n    let label: String\n    let destructive: Bool\n    let callback: () -> Void\n    \n    public init(\n        label: LocalizedStringResource,\n        destructive: Bool = false,\n        callback: @escaping () -> Void\n    ) {\n        self.label = .init(localized: label)\n        self.destructive = destructive\n        self.callback = callback\n    }\n    \n    @_disfavoredOverload\n    public init(\n        label: String,\n        destructive: Bool = false,\n        callback: @escaping () -> Void\n    ) {\n        self.label = label\n        self.destructive = destructive\n        self.callback = callback\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Sources/QuickSwipes/QuickSwipeThresholdSet.swift",
    "content": "//\n//  SwipeBehavior.swift\n//  QuickSwipes\n//\n//  Created by Sjmarf on 2025-08-22.\n//\n\nimport Foundation\n\npublic struct QuickSwipeThresholdSet {\n    /// Minimum distance to trigger the primary action\n    public let primary: CGFloat\n    /// Minimum distance to trigger the secondary action\n    public let secondary: CGFloat\n    /// Minimum distance to trigger the tertiary action\n    public let tertiary: CGFloat\n    \n    public var all: [CGFloat] { [primary, secondary, tertiary] }\n    \n    public init(primary: CGFloat, secondary: CGFloat, tertiary: CGFloat) {\n        self.primary = primary\n        self.secondary = secondary\n        self.tertiary = tertiary\n    }\n    \n    public static var `default`: QuickSwipeThresholdSet {\n        .init(primary: 60, secondary: 150, tertiary: 240)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Sources/QuickSwipes/QuickSwipesViewModifier.swift",
    "content": "//\n//  QuickSwipesViewModifier.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2025-03-23.\n//\n\nimport ComponentViews\nimport Haptics\nimport Icons\nimport MlemLogger\nimport os\nimport SwiftUI\nimport Theming\n\n// swiftlint:disable:next type_body_length\nstruct QuickSwipeViewModifier: ViewModifier {\n    let log: Logger = .mlemLogger()\n    \n    @Environment(HapticManager.self) var hapticManager\n    @Environment(\\.palette) var palette\n    \n    @Environment(\\.quickSwipeThresholdSet) var thresholds\n    @Environment(\\.quickSwipeMinimumDrag) var minimumDrag\n    @Environment(\\.quickSwipeIconSize) var iconSize\n    @Environment(\\.quickSwipeCornerRadius) var cornerRadius\n    @Environment(\\.quickSwipesEnabled) var quickSwipesEnabled\n    \n    // state\n    @GestureState var dragState: CGFloat = .zero\n    @State var dragPosition: CGFloat = .zero\n    @State var prevDragPosition: CGFloat = .zero\n    @State var dragBackground: ThemedColor? = .themedBackground\n    @State var leadingSwipeIcon: Icon?\n    @State var trailingSwipeIcon: Icon?\n    @State var iconIsActive: Bool = false\n    @State var activeChoiceGroup: QuickSwipeChoiceGroup?\n    \n    let config: SwipeConfiguration\n    \n    private var primaryLeadingAction: QuickSwipeAction? { config.leadingActions.first }\n    private var primaryTrailingAction: QuickSwipeAction? { config.trailingActions.first }\n    \n    init(config: SwipeConfiguration) {\n        self.config = config\n        \n        _leadingSwipeIcon = State(initialValue: primaryLeadingAction?.icon)\n        _trailingSwipeIcon = State(initialValue: primaryTrailingAction?.icon)\n    }\n    \n    func body(content: Content) -> some View {\n        if quickSwipesEnabled {\n            innerBody(content: content)\n                .clipShape(.rect(cornerRadius: cornerRadius)) // clip slidable card\n                .background(shadowBackground)\n                .geometryGroup()\n                .offset(x: dragPosition) // using dragPosition so we can apply withAnimation() to it\n                .background(iconBackground)\n                // disables links from highlighting when tapped\n                .buttonStyle(.empty)\n                .clipShape(.rect(cornerRadius: cornerRadius)) // clip entire view\n                .versionAwareDialog(\n                    activeChoiceGroup?.title ?? \"\",\n                    isPresented: .init(get: { activeChoiceGroup != nil }, set: { _ in activeChoiceGroup = nil })\n                ) {\n                    ForEach(Array((activeChoiceGroup?.items ?? []).enumerated()), id: \\.offset) { _, item in\n                        Button(item.label, role: item.destructive ? .destructive : nil, action: item.callback)\n                    }\n                    Button(\"Cancel\", role: .cancel) {}\n                }\n        } else {\n            content\n                .clipShape(.rect(cornerRadius: cornerRadius)) // clip entire view\n        }\n    }\n    \n    @ViewBuilder\n    func innerBody(content: Content) -> some View {\n        content\n            .gesture(\n                PanGesture(leadingBuffer: 70) { recognizer in\n                    if [.ended, .cancelled].contains(recognizer.state) {\n                        draggingUpdated(dragState: 0)\n                    } else {\n                        draggingUpdated(dragState: recognizer.translation(in: recognizer.view).x)\n                    }\n                }\n            )\n    }\n    \n    var shadowBackground: some View {\n        // creates a shadow under the edge of the view\n        Rectangle()\n            .foregroundStyle(.clear)\n            .border(width: 10, edges: [.leading, .trailing], color: .black)\n            .clipShape(RoundedRectangle(cornerRadius: cornerRadius))\n            .shadow(radius: 5)\n            .opacity(dragPosition == .zero ? 0 : 1) // prevent this view from appearing in animations on parent view(s).\n    }\n    \n    var iconBackground: some View {\n        dragBackground?.resolve(with: palette)\n            .overlay {\n                HStack(spacing: 0) {\n                    if dragPosition > 0 {\n                        iconView(leadingSwipeIcon)\n                    }\n                    Spacer()\n                    if dragPosition < 0 {\n                        iconView(trailingSwipeIcon)\n                    }\n                }\n                .accessibilityHidden(true) // prevent these from popping up in VO\n                .opacity(dragPosition == .zero ? 0 : 1) // prevent this view from appearing in animations on parent view(s).\n            }\n    }\n    \n    func iconView(_ icon: Icon?) -> some View {\n        Image(icon: icon?.representingState(active: iconIsActive) ?? .general.warning)\n            .font(.system(size: iconSize))\n            .foregroundStyle(.themedContrastingLabel)\n            .frame(width: iconWidth)\n            .padding(.horizontal, iconWidth)\n    }\n    \n    private func draggingUpdated(dragState: CGFloat) {\n        // if dragState changes and is now 0, gesture has ended; compute action based on last detected position\n        if dragState == .zero {\n            draggingDidEnd()\n        } else {\n            guard shouldRespondToDragPosition(dragState) else {\n                // as swipe actions are optional we don't allow dragging without a primary action\n                return\n            }\n            \n            // update position\n            dragPosition = dragState\n            \n            let edgeForActions = edgeForActions(at: dragPosition)\n            let actionIndex = actionIndex(edge: edgeForActions, at: dragPosition)\n            let action = action(edge: edgeForActions, index: actionIndex)\n            let threshold = actionThreshold(edge: edgeForActions, index: actionIndex)\n            \n            // update color and symbol. If crossed an edge, play a gentle haptic\n            switch edgeForActions {\n            case .leading:\n                if actionIndex == nil {\n                    iconIsActive = false\n                    leadingSwipeIcon = primaryLeadingAction?.icon\n                    dragBackground = primaryLeadingAction?.color.opacity(dragPosition / threshold)\n                } else {\n                    iconIsActive = true\n                    leadingSwipeIcon = action?.icon\n                    dragBackground = action?.color.opacity(dragPosition / threshold)\n                }\n            case .trailing:\n                if actionIndex == nil {\n                    iconIsActive = false\n                    trailingSwipeIcon = primaryTrailingAction?.icon\n                    dragBackground = primaryTrailingAction?.color.opacity(dragPosition / threshold)\n                } else {\n                    iconIsActive = true\n                    trailingSwipeIcon = action?.icon\n                    dragBackground = action?.color.opacity(dragPosition / threshold)\n                }\n            }\n            \n            // If crossed an edge, play a gentle haptic\n            let previousIndex = self.actionIndex(edge: edgeForActions, at: prevDragPosition)\n            let currentIndex = self.actionIndex(edge: edgeForActions, at: dragPosition)\n            if let hapticInfo = hapticInfo(transitioningFrom: previousIndex, to: currentIndex) {\n                hapticManager.play(haptic: hapticInfo.0, tier: hapticInfo.1)\n            }\n                          \n            prevDragPosition = dragPosition\n        }\n    }\n    \n    private func draggingDidEnd() {\n        let finalDragPosition = prevDragPosition\n        \n        reset()\n        \n        let action = swipeAction(at: finalDragPosition)\n        \n        switch action?.perform {\n        case let .callback(callback, confirmationPrompt):\n            if let confirmationPrompt {\n                activeChoiceGroup = .init(\n                    title: confirmationPrompt,\n                    items: [.init(label: \"Confirm\", destructive: true, callback: callback)]\n                )\n            } else {\n                callback()\n            }\n        case let .choice(choiceGroup):\n            activeChoiceGroup = choiceGroup\n        case nil:\n            break\n        }\n    }\n    \n    private func reset() {\n        withAnimation(.spring(response: 0.25)) {\n            dragPosition = .zero\n            prevDragPosition = .zero\n            leadingSwipeIcon = primaryLeadingAction?.icon\n            trailingSwipeIcon = primaryTrailingAction?.icon\n            dragBackground = .themedBackground\n        }\n    }\n    \n    private func shouldRespondToDragPosition(_ position: CGFloat) -> Bool {\n        if position > 0, primaryLeadingAction == nil {\n            return false\n        }\n        \n        if position < 0, primaryTrailingAction == nil {\n            return false\n        }\n        \n        return true\n    }\n    \n    // MARK: -\n    \n    /// Get the swipe action a specific drag position.\n    /// - Parameter dragPosition: Along the x-axis.\n    private func swipeAction(at dragPosition: CGFloat) -> (QuickSwipeAction)? {\n        let edge = edgeForActions(at: dragPosition)\n        let index = actionIndex(edge: edge, at: dragPosition)\n        let action = action(edge: edge, index: index)\n        return action\n    }\n    \n    /// For a particular `dragPosition`, returns the relevant edge for which to show/perform actions.\n    private func edgeForActions(at dragPosition: CGFloat) -> HorizontalEdge {\n        dragPosition > 0 ? .leading : .trailing\n    }\n    \n    /// Index of the action along the specified edge at the specified drag position.\n    /// - Returns: A `nil` value denotes the state where swiping has begun, but not enough to trigger any actions.\n    private func actionIndex(edge: HorizontalEdge, at dragPosition: CGFloat) -> Array<CGFloat>.Index? {\n        /// Map a `dragPosition` to a `dragThreshold`, which tells us what swipe action to perform, where `nil` is no action, `1` is primary, `2` is secondary, etc.\n        let thresholdIndex = thresholds.all.lastIndex {\n            switch edge {\n            case .leading:\n                return dragPosition > $0\n            case .trailing:\n                return dragPosition < -$0\n            }\n        }\n        \n        guard let thresholdIndex else {\n            return nil\n        }\n        \n        /// There may not be an associated action for a threshold.\n        switch edge {\n        case .leading:\n            if thresholdIndex > (config.leadingActions.endIndex - 1) {\n                log.debug(\"leading action not configured for this threshold\")\n                return config.leadingActions.endIndex - 1\n            }\n            return thresholdIndex\n        case .trailing:\n            if thresholdIndex > (config.trailingActions.endIndex - 1) {\n                log.debug(\"trailing action not configured for this threshold\")\n                return config.trailingActions.endIndex - 1\n            }\n            return thresholdIndex\n        }\n    }\n    \n    /// Get the action associated with an edge at the specified index.\n    private func action(edge: HorizontalEdge, index actionIndex: Array<CGFloat>.Index?) -> (QuickSwipeAction)? {\n        guard let actionIndex else {\n            return nil\n        }\n        switch edge {\n        case .leading:\n            return config.leadingActions[safeIndex: actionIndex]\n        case .trailing:\n            return config.trailingActions[safeIndex: actionIndex]\n        }\n    }\n    \n    /// Maps swipe action transitions into an appropriate haptic info payload for haptic playback purposes.\n    /// - No-op if both indexes are the same (i.e. transition isn't happening).\n    private func hapticInfo(\n        transitioningFrom previousIndex: Array<CGFloat>.Index?,\n        to currentIndex: Array<CGFloat>.Index?\n    ) -> (Haptic, HapticTier)? {\n        guard previousIndex != currentIndex else {\n            /// Same action, don't play haptic.\n            return nil\n        }\n        \n        // From nil -> 0 -> 1 -> 2, etc, where nil is no action, and 0 is the primary action.\n        // Swiping towards to primary action.\n        // Index values are always >= 0 for both leading/trailing edges.\n        // Since nil indicates no action, we use -1 to represent nil instead (lol, yes).\n        if (currentIndex ?? -1) < (previousIndex ?? -1) {\n            return (.mushyInfo, .low)\n        } else {\n            if previousIndex == nil {\n                return (.gentleInfo, .high)\n            } else if previousIndex == 1 {\n                return (.firmInfo, .high)\n            } else {\n                return (.firmInfo, .high)\n            }\n        }\n    }\n    \n    /// Get the threshold (in screen points) required to trigger a particular action.\n    /// - Parameter edge: Show actions on this edge.\n    /// - Parameter index: Index of the action in question.\n    /// - Returns: Negative values for trailing actions along the x-axis.\n    private func actionThreshold(\n        edge edgeForActions: HorizontalEdge,\n        index actionIndex: Array<CGFloat>.Index?\n    ) -> CGFloat {\n        guard let actionIndex else {\n            switch edgeForActions {\n            case .leading:\n                return thresholds.primary\n            case .trailing:\n                return -thresholds.primary\n            }\n        }\n        \n        switch edgeForActions {\n        case .leading:\n            return thresholds.all[actionIndex]\n        case .trailing:\n            return -thresholds.all[actionIndex]\n        }\n    }\n    \n    private var iconWidth: CGFloat {\n        // this sets the icon to always be centered between the edge of the background and the edge of the swipeable item, as this is\n        // both the width of the icon's frame and its padding. the actual icon size is done using fonts.\n        thresholds.primary / 3\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Sources/QuickSwipes/SwipeConfiguration.swift",
    "content": "//\n//  SwipeConfiguration.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-11.\n//\n\nimport Foundation\n\npublic struct SwipeConfiguration {\n    /// In ascending order of appearance.\n    public let leadingActions: [QuickSwipeAction]\n    /// In ascending order of appearance.\n    public let trailingActions: [QuickSwipeAction]\n    \n    public init(\n        leadingActions: [QuickSwipeAction] = [],\n        trailingActions: [QuickSwipeAction] = []\n    ) {\n        assert(\n            leadingActions.count <= 3 && trailingActions.count <= 3,\n            \"Too many swipe actions!\"\n        )\n        \n        self.leadingActions = leadingActions.filter(\\.enabled)\n        self.trailingActions = trailingActions.filter(\\.enabled)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Sources/QuickSwipes/View+EdgeBorders.swift",
    "content": "//\n//  View+EdgeBorders.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-06-10.\n//\n// source: https://stackoverflow.com/questions/58632188/swiftui-add-border-to-one-edge-of-an-image\n\nimport Foundation\nimport SwiftUI\n\nextension View {\n    func border(width: CGFloat, edges: [Edge], color: Color) -> some View {\n        overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color))\n    }\n}\n\nstruct EdgeBorder: Shape {\n    var width: CGFloat\n    var edges: [Edge]\n\n    func path(in rect: CGRect) -> Path {\n        edges.map { edge -> Path in\n            switch edge {\n            case .top: return Path(.init(x: rect.minX, y: rect.minY, width: rect.width, height: width))\n            case .bottom: return Path(.init(x: rect.minX, y: rect.maxY - width, width: rect.width, height: width))\n            case .leading: return Path(.init(x: rect.minX, y: rect.minY, width: width, height: rect.height))\n            case .trailing: return Path(.init(x: rect.maxX - width, y: rect.minY, width: width, height: rect.height))\n            }\n        }.reduce(into: Path()) { $0.addPath($1) }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Sources/QuickSwipes/View+Extensions.swift",
    "content": "//\n//  File.swift\n//  QuickSwipes\n//\n//  Created by Sjmarf on 2025-08-22.\n//\n\nimport SwiftUI\n\npublic extension View {\n    func quickSwipeThresholds(_ thresholdSet: QuickSwipeThresholdSet) -> some View {\n        environment(\\.quickSwipeThresholdSet, thresholdSet)\n    }\n    \n    func quickSwipeThresholds(primary: CGFloat, secondary: CGFloat, tertiary: CGFloat) -> some View {\n        environment(\\.quickSwipeThresholdSet, .init(primary: primary, secondary: secondary, tertiary: tertiary))\n    }\n    \n    func quickSwipeMinimumDrag(_ minimumDrag: CGFloat) -> some View {\n        environment(\\.quickSwipeMinimumDrag, minimumDrag)\n    }\n    \n    func quickSwipeIconSize(_ iconSize: CGFloat) -> some View {\n        environment(\\.quickSwipeIconSize, iconSize)\n    }\n    \n    func quickSwipeCornerRadius(_ cornerRadius: CGFloat) -> some View {\n        environment(\\.quickSwipeCornerRadius, cornerRadius)\n    }\n    \n    func quickSwipesDisabled(_ disabled: Bool = true) -> some View {\n        environment(\\.quickSwipesEnabled, !disabled)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/QuickSwipes/Sources/QuickSwipes/View+QuickSwipes.swift",
    "content": "//\n//  View+QuickSwipes.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2023-06-20.\n//\n\nimport SwiftUI\nimport Theming\n\npublic extension View {\n    /// Adds quick swipes to a view.\n    ///\n    /// NOTE: if the view you are attaching this to also has a context menu, add the context menu view modifier AFTER the quick swipes modifier! This will prevent the quick swipe from triggering and appearing bugged on an aborted context menu pop if the context menu animation initiates.\n    /// - Parameters:\n    ///   - leading: leading edge quick swipes, ordered by ascending swipe distance from leading edge\n    ///   - trailing: trailing edge quick swipes, ordered by ascending swipe distance from leading edge\n    @ViewBuilder\n    func quickSwipes(\n        leading: [QuickSwipeAction] = [],\n        trailing: [QuickSwipeAction] = []\n    ) -> some View {\n        modifier(\n            QuickSwipeViewModifier(\n                config: .init(\n                    leadingActions: leading,\n                    trailingActions: trailing\n                )\n            )\n        )\n    }\n    \n    @ViewBuilder\n    func quickSwipes(_ config: SwipeConfiguration) -> some View {\n        modifier(QuickSwipeViewModifier(config: config))\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/.gitignore",
    "content": ".DS_Store\n/.build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\n"
  },
  {
    "path": "Mlem/Packages/Rest/Package.resolved",
    "content": "{\n  \"originHash\" : \"433eb46e437fba071b47444bcf712c57872e1973de4812d146c7db18e50834e2\",\n  \"pins\" : [\n    {\n      \"identity\" : \"libwebp-xcode\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/libwebp-Xcode.git\",\n      \"state\" : {\n        \"revision\" : \"0d60654eeefd5d7d2bef3835804892c40225e8b2\",\n        \"version\" : \"1.5.0\"\n      }\n    },\n    {\n      \"identity\" : \"nuke\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/kean/Nuke.git\",\n      \"state\" : {\n        \"revision\" : \"0ead44350d2737db384908569c012fe67c421e4d\",\n        \"version\" : \"12.8.0\"\n      }\n    },\n    {\n      \"identity\" : \"sdwebimage\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/SDWebImage.git\",\n      \"state\" : {\n        \"revision\" : \"cac9a55a3ae92478a2c95042dcc8d9695d2129ca\",\n        \"version\" : \"5.21.0\"\n      }\n    },\n    {\n      \"identity\" : \"sdwebimagewebpcoder\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/SDWebImage/SDWebImageWebPCoder\",\n      \"state\" : {\n        \"revision\" : \"f534cfe830a7807ecc3d0332127a502426cfa067\",\n        \"version\" : \"0.14.6\"\n      }\n    }\n  ],\n  \"version\" : 3\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"Rest\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"Rest\",\n            targets: [\"Rest\", \"URLEncoder\"]\n        )\n    ],\n    dependencies: [\n        .package(path: \"../MlemLogger\")\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"Rest\",\n            dependencies: [\n                .byName(name: \"MlemLogger\"),\n                .byName(name: \"URLEncoder\")\n            ],\n            swiftSettings: [\n                .swiftLanguageMode(.v5),\n                .enableUpcomingFeature(\"FullTypedThrows\"),\n                .enableUpcomingFeature(\"BareSlashRegexLiterals\")\n            ]\n        ),\n        .target(\n            name: \"URLEncoder\",\n            dependencies: [\n                .byName(name: \"MlemLogger\")\n            ],\n            swiftSettings: [\n                .swiftLanguageMode(.v5),\n                .enableUpcomingFeature(\"FullTypedThrows\"),\n                .enableUpcomingFeature(\"BareSlashRegexLiterals\")\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/Rest/ApiRequest.swift",
    "content": "//\n//  ApiRequest.swift\n//  Mlem\n//\n//  Created by Nicholas Lawson on 06/06/2023.\n//\n\nimport Foundation\nimport URLEncoder\n\n// MARK: - RestRequest\n\npublic protocol RestRequest {\n    associatedtype Response: Decodable\n\n    var path: String { get }\n    var headers: [String: String] { get }\n    \n    func endpoint(\n        base: URL,\n        encoderUserInfo: [CodingUserInfoKey: any Sendable],\n        convertParamsToSnakeCase: Bool\n    ) throws(URLQueryItemEncoderError) -> URL\n}\n\npublic extension RestRequest {\n    var headers: [String: String] { defaultHeaders }\n\n    var defaultHeaders: [String: String] {\n        [\"Content-Type\": \"application/json\"]\n    }\n}\n\n// MARK: - GetRequest\n\npublic protocol GetRequest: RestRequest {\n    associatedtype Parameters: Encodable\n    var parameters: Parameters? { get }\n}\n\npublic extension RestRequest {\n    func endpoint(\n        base: URL,\n        encoderUserInfo: [CodingUserInfoKey: any Sendable],\n        convertParamsToSnakeCase: Bool\n    ) throws(URLQueryItemEncoderError) -> URL {\n        base\n            .appending(path: path)\n    }\n}\n\npublic extension GetRequest {\n    func endpoint(\n        base: URL,\n        encoderUserInfo: [CodingUserInfoKey: any Sendable],\n        convertParamsToSnakeCase: Bool\n    ) throws(URLQueryItemEncoderError) -> URL {\n        if let parameters {\n            try base\n                .appending(path: path)\n                .appending(queryItems: URLQueryItemEncoder.encode(\n                    parameters,\n                    convertToSnakeCase: convertParamsToSnakeCase,\n                    userInfo: encoderUserInfo\n                ))\n        } else {\n            base\n                .appending(path: path)\n        }\n    }\n}\n\n// MARK: - RequestWithBody\n\npublic enum RequestWithBodyMethod {\n    case post, put, delete\n    \n    var stringValue: String {\n        switch self {\n        case .post: \"POST\"\n        case .put: \"PUT\"\n        case .delete: \"DELETE\"\n        }\n    }\n}\n\npublic protocol RequestWithBody: RestRequest {\n    associatedtype Body: Encodable\n    var body: Body? { get }\n    var method: RequestWithBodyMethod { get }\n}\n\npublic protocol PostRequest: RequestWithBody { }\n\npublic extension PostRequest {\n    var method: RequestWithBodyMethod { .post }\n}\n\npublic protocol PutRequest: RequestWithBody { }\n\npublic extension PutRequest {\n    var method: RequestWithBodyMethod { .put }\n}\n\npublic protocol DeleteRequest: RequestWithBody { }\n\npublic extension DeleteRequest {\n    var method: RequestWithBodyMethod { .delete }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/Rest/ImageUploadDelegate.swift",
    "content": "//\n//  File.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-07-05.\n//\n\nimport Foundation\n\npublic class ImageUploadDelegate: NSObject, URLSessionTaskDelegate {\n    let callback: (Double) -> Void\n    \n    public init(callback: @escaping (Double) -> Void) {\n        self.callback = callback\n    }\n    \n    public func urlSession(\n        _ session: URLSession,\n        task: URLSessionTask,\n        didSendBodyData bytesSent: Int64,\n        totalBytesSent: Int64,\n        totalBytesExpectedToSend: Int64\n    ) {\n        callback(Double(totalBytesSent) / Double(totalBytesExpectedToSend))\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/Rest/JSONDecoder+Extensions.swift",
    "content": "//\n//  JSONDecoder+Extensions.swift\n//  Mlem\n//\n//  Created by Nicholas Lawson on 11/06/2023.\n//\n\nimport Foundation\n\npublic extension JSONDecoder {\n    static var defaultDecoder: JSONDecoder {\n        let decoder = JSONDecoder()\n        \n        let formats = [\n            \"yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ\",\n            \"yyyy-MM-dd'T'HH:mm:ss.SSSSSS\",\n            \"yyyy-MM-dd'T'HH:mm:ssZ\",\n            \"yyyy-MM-dd'T'HH:mm:ss\"\n        ]\n        \n        let formatters = formats.map { format in\n            let formatter = DateFormatter()\n            formatter.timeZone = .gmt\n            formatter.locale = Locale(identifier: \"en_US_POSIX\")\n            formatter.dateFormat = format\n            return formatter\n        }\n\n        decoder.dateDecodingStrategy = .custom { decoder in\n            let container = try decoder.singleValueContainer()\n            let string = try container.decode(String.self)\n\n            for formatter in formatters {\n                if let date = formatter.date(from: string) {\n                    return date\n                }\n            }\n\n            // after some discussion we've agreed to fail the modelling if the date\n            // does match _any_ of the above, as based on the current API source code\n            // it should be one of those\n            throw Swift.DecodingError.dataCorrupted(\n                .init(\n                    codingPath: container.codingPath,\n                    debugDescription: \"Failed to parse date\"\n                )\n            )\n        }\n        return decoder\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/Rest/JSONEncoder+Extensions.swift",
    "content": "//\n//  File.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-10-14.\n//\n\nimport Foundation\n\nextension JSONEncoder.DateEncodingStrategy {\n    static var iso8601WithMilliseconds: Self {\n        .custom { date, encoder in\n            let formatter = ISO8601DateFormatter()\n            // `.withFractionalSeconds` is required for the PieFed banFromCommunity request\n            formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]\n            var container = encoder.singleValueContainer()\n            try container.encode(formatter.string(from: date))\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/Rest/MlemUrlRequest.swift",
    "content": "//\n//  MlemUrlRequest.swift\n//  MlemMiddleware\n//\n//  Created by Eric Andrews on 2025-03-12.\n//\n\nimport Foundation\n\npublic func mlemUrlRequest(url: URL) -> URLRequest {\n    var url = url\n    // .gifv is secretly just mp4; replacing the extension here ensures it is picked up by the NukeVideo decoder\n    if url.pathExtension == \"gifv\" {\n        if let fixedUrl: URL = .init(string: url.absoluteString.replacingOccurrences(of: \".gifv\", with: \".mp4\")) {\n            url = fixedUrl\n        } else {\n            assertionFailure(\"Could not create fixed URL for \\(url)\")\n        }\n    }\n    var ret = URLRequest(url: url)\n    ret.addValue(\"MlemUserAgent\", forHTTPHeaderField: \"User-Agent\")\n    return ret\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/Rest/MultiPartForm.swift",
    "content": "//\n//  File.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-07-05.\n//  \n\nimport Foundation\n\n// swiftlint:disable:next function_parameter_count\npublic func createMultiPartForm(\n    boundary: String,\n    contentType: String,\n    name: String,\n    fileName: String,\n    imageData: Data,\n    auth: String\n) -> Data {\n    var data = Data()\n    data.append(Data(\"--\\(boundary)\\r\\n\".utf8))\n    data.append(Data(\"Content-Disposition: form-data; name=\\\"\\(name)\\\"; filename=\\\"\\(fileName)\\\"\\r\\n\".utf8))\n    data.append(Data(\"Content-Type: \\(contentType)\\r\\n\\r\\n\".utf8))\n    data.append(imageData)\n    data.append(Data(\"\\r\\n--\\(boundary)--\\r\\n\".utf8))\n    return data\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/Rest/RestClient.swift",
    "content": "//\n//  RestClient\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-04.\n//\n\nimport Foundation\n\npublic class RestClient {\n    public struct ErrorProcessorContext {\n        let decoder: JSONDecoder\n        let data: Data\n        let response: HTTPURLResponse\n    }\n\n    public var decoder: JSONDecoder\n    public var convertParamsToSnakeCase: Bool = true\n    \n    // This should really be internal, but for now the image upload system needs to access this\n    public let urlSession: URLSession = .init(configuration: .default)\n\n    public var errorProcessor: (ErrorProcessorContext) throws(RestError) -> Void\n    \n    public init(\n        errorProcessor: @escaping (ErrorProcessorContext) throws(RestError) -> Void = { _ in },\n        convertParamsToSnakeCase: Bool = true,\n        decoder: JSONDecoder = .defaultDecoder\n    ) {\n        self.errorProcessor = errorProcessor\n        self.convertParamsToSnakeCase = convertParamsToSnakeCase\n        self.decoder = decoder\n    }\n\n    public init<ErrorType: Decodable & CustomStringConvertible>(\n        errorType: ErrorType.Type,\n        convertParamsToSnakeCase: Bool = true,\n        decoder: JSONDecoder = .defaultDecoder\n    ) {\n        self.errorProcessor = { context throws(RestError) in\n            if let apiError = try? context.decoder.decode(ErrorType.self, from: context.data) {\n                // at present we have a single error model which appears to be used throughout\n                // the API, however we may way to consider adding the error model type as an\n                // associated value in the same was as the response to allow requests to define\n                // their own error models when necessary, or drop back to this as the default...\n                \n                throw .response(String(describing: apiError), statusCode: context.response.statusCode)\n            }\n        }\n        self.convertParamsToSnakeCase = convertParamsToSnakeCase\n        self.decoder = decoder\n    }\n    \n    public func perform<Request: RestRequest>(\n        baseUrl: URL,\n        _ request: Request,\n        token: String?,\n        encoderUserInfo: [CodingUserInfoKey: any Sendable] = [:]\n    ) async throws(RestError) -> Request.Response {\n        let urlRequest = try urlRequest(\n            baseUrl: baseUrl,\n            request: request,\n            token: token,\n            encoderUserInfo: encoderUserInfo\n        )\n        // this line intentionally left commented for convenient future debugging\n        // urlRequest.debug()\n        let (data, response) = try await execute(urlRequest)\n        if let response = response as? HTTPURLResponse {\n            if response.statusCode >= 500 || response.statusCode == 404 {\n                throw .serverError(statusCode: response.statusCode)\n            }\n\n            try errorProcessor(\n                .init(\n                    decoder: decoder,\n                    data: data,\n                    response: response\n                )\n            )\n        }\n        \n        return try decode(Request.Response.self, from: data)\n    }\n    \n    public func execute(_ urlRequest: URLRequest) async throws(RestError) -> (Data, URLResponse) {\n        do {\n            return try await urlSession.data(for: urlRequest)\n        } catch {\n            if case URLError.cancelled = error as NSError {\n                throw .cancelled\n            } else {\n                throw .networking(error)\n            }\n        }\n    }\n    \n    func urlRequest(\n        baseUrl: URL,\n        request: any RestRequest,\n        token: String?,\n        encoderUserInfo: [CodingUserInfoKey: any Sendable] = [:]\n    ) throws(RestError) -> URLRequest {\n        let url: URL\n        do {\n            url = try request.endpoint(\n                base: baseUrl,\n                encoderUserInfo: encoderUserInfo,\n                convertParamsToSnakeCase: convertParamsToSnakeCase\n            )\n        } catch {\n            throw .parameterEncoding(error)\n        }\n        \n        var urlRequest = mlemUrlRequest(url: url)\n        urlRequest.cachePolicy = .reloadIgnoringLocalCacheData\n        for header in request.headers {\n            urlRequest.setValue(header.value, forHTTPHeaderField: header.key)\n        }\n        \n        if request is any GetRequest {\n            urlRequest.httpMethod = \"GET\"\n        } else if let postDefinition = request as? any RequestWithBody {\n            urlRequest.httpMethod = postDefinition.method.stringValue\n            urlRequest.httpBody = try createBodyData(for: postDefinition, encoderUserInfo: encoderUserInfo)\n        }\n        \n        if let token {\n            urlRequest.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n        }\n        \n        return urlRequest\n    }\n    \n    func createBodyData(\n        for defintion: any RequestWithBody,\n        encoderUserInfo: [CodingUserInfoKey: any Sendable] = [:]\n    ) throws(RestError) -> Data {\n        do {\n            let encoder = JSONEncoder()\n            encoder.dateEncodingStrategy = .iso8601WithMilliseconds\n            encoder.userInfo = encoderUserInfo\n            let body = defintion.body ?? \"\"\n            return try encoder.encode(body)\n        } catch {\n            throw .encoding(error)\n        }\n    }\n    \n    private func decode<T: Decodable>(_ model: T.Type, from data: Data) throws(RestError) -> T {\n        do {\n            return try decoder.decode(model, from: data)\n        } catch {\n            throw .decoding(data, error)\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/Rest/RestError.swift",
    "content": "//\n//  File.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-06-04.\n//\n\nimport Foundation\nimport URLEncoder\n\npublic enum RestError: Error {\n    case serverError(statusCode: Int) // Should always be a 5xx status code\n    case response(String, statusCode: Int)\n    case encoding(Error)\n    case parameterEncoding(URLQueryItemEncoderError)\n    case decoding(Data, Error?)\n    case networking(Error)\n    case cancelled\n}\n\nextension RestError: CustomStringConvertible {\n    public var description: String {\n        switch self {\n        case let .encoding(error):\n            return \"Unable to encode: \\(error)\"\n        case let .networking(error):\n            return \"Networking error: \\(error)\"\n        case let .response(errorResponse, status):\n            return \"Response error: \\(errorResponse) with status \\(status)\"\n        case let .serverError(statusCode):\n            return \"Server Error: \\(statusCode)\"\n        case .cancelled:\n            return \"Cancelled\"\n        case let .decoding(data, error):\n            guard let string = String(data: data, encoding: .utf8) else {\n                return localizedDescription\n            }\n            if let error {\n                return \"Unable to decode: \\(string)\\nError: \\(error)\"\n            }\n            return \"Unable to decode: \\(string)\"\n        case let .parameterEncoding(error):\n            return \"Unable to encode request parameters: \\(error)\"\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/Rest/URLRequest+Extensions.swift",
    "content": "//\n//  URLRequest+Extensions.swift\n//\n//\n//  Created by Eric Andrews on 2024-07-03.\n//\n// https://stackoverflow.com/questions/34705449/how-to-print-http-request-to-console\n\nimport Foundation\nimport MlemLogger\nimport os\n\nextension URLRequest {\n    /// Prints this URLRequest in human-readable form\n    func debug() {\n        let statement = \"\"\"\n        \\(httpMethod!) \\(url!)\n        \"Headers:\"\n        \\(allHTTPHeaderFields ?? [:])\n        \"Body:\"\n        \\(String(data: httpBody ?? Data(), encoding: .utf8)!)\n        \"\"\"\n        Logger.universal.debug(\"\\(statement)\")\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/URLEncoder/InternalQueryItemEncoder.swift",
    "content": "//\n//  File.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-11-14.\n//  \n\nimport Foundation\n\ninternal class InternalURLQueryItemEncoder: Encoder {\n    var queryParams: [URLQueryItem] = .init()\n\n    // This is just for conformance to Encoder. This never gets modified because we\n    // disallow nested containers\n    let codingPath: [CodingKey] = []\n    \n    let userInfo: [CodingUserInfoKey: Any] \n    let settings: URLQueryItemEncoderSettings\n\n    init(userInfo: [CodingUserInfoKey: Any], settings: URLQueryItemEncoderSettings) {\n        self.userInfo = userInfo\n        self.settings = settings\n    }\n\n    func singleValueContainer() -> SingleValueEncodingContainer {\n        // This value throws an error as soon as you try to encode with it\n        ThrowingSingleValueContainer(encoder: self)\n    }\n\n    func unkeyedContainer() -> UnkeyedEncodingContainer {\n        // This value throws an error as soon as you try to encode with it\n        ThrowingUnkeyedContainer(encoder: self)\n    }\n\n    func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {\n        KeyedEncodingContainer(TopLevelKeyedContainer<Key>(encoder: self, settings: settings))\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/URLEncoder/RetrievalEncoder.swift",
    "content": "//\n//  RetrievalEncoder.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-11-14.\n//  \n\nimport Foundation\n\ninternal class RetrievalEncoder: Encoder {\n    // This is just for conformance to Encoder. This never gets modified because we\n    // disallow nested containers\n    let codingPath: [CodingKey] = []\n    \n    // Just for conformance; unused\n    var userInfo: [CodingUserInfoKey: Any]\n\n    var encodedValue: (any Encodable)?\n\n    init(userInfo: [CodingUserInfoKey: Any]) {\n        self.userInfo = userInfo\n    }\n\n    func singleValueContainer() -> SingleValueEncodingContainer {\n        // This value throws an error as soon as you try to encode with it\n        RetrievalSingleValueContainer(encoder: self)\n    }\n\n    func unkeyedContainer() -> UnkeyedEncodingContainer {\n        // This value throws an error as soon as you try to encode with it\n        ThrowingUnkeyedContainer(encoder: self)\n    }\n\n    func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {\n        KeyedEncodingContainer(ThrowingKeyedContainer<Key>(encoder: self))\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/URLEncoder/RetrievalSingleValueContainer.swift",
    "content": "//\n//  RetrievalSingleValueContainer.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-11-14.\n//  \n\nimport Foundation\n\ninternal class RetrievalSingleValueContainer: SingleValueEncodingContainer {\n    let encoder: RetrievalEncoder\n    let codingPath: [CodingKey] = []\n\n    init(encoder: RetrievalEncoder) {\n        self.encoder = encoder\n    }\n\n    func encodeNil() throws {}\n\n    func encode(_ value: some Encodable) throws {\n        encoder.encodedValue = value\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/URLEncoder/String+Extensions.swift",
    "content": "//\n//  File.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-11-14.\n//  \n\nimport Foundation\n\ninternal extension String {\n    func camelToSnakeCase() -> String {\n        replacing(/([a-z])([A-Z])/) { \"\\($0.output.1)_\\($0.output.2)\"\n        }.lowercased()\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/URLEncoder/ThrowingKeyedContainer.swift",
    "content": "//\n//  File.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-11-14.\n//  \n\nimport Foundation\n\ninternal class ThrowingKeyedContainer<K: CodingKey>: KeyedEncodingContainerProtocol {\n    var encoder: any Encoder\n\n    init(encoder: any Encoder) {\n        self.encoder = encoder\n    }\n\n    var codingPath: [CodingKey] = []\n\n    func encodeNil(forKey key: K) throws {}\n\n    func encode(_ value: some Encodable, forKey key: K) throws {\n        throw URLQueryItemEncoderError.nestedContainersUnsupported\n    }\n\n    func nestedContainer<NestedKey>(\n        keyedBy type: NestedKey.Type, forKey key: K\n    ) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {\n        assertionFailure(\"We should throw an error *before* this gets called\")\n        return KeyedEncodingContainer(ThrowingKeyedContainer<NestedKey>(encoder: encoder))\n    }\n\n    func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer {\n        assertionFailure(\"We should throw an error *before* this gets called\")\n        return ThrowingUnkeyedContainer(encoder: encoder)\n    }\n\n    func superEncoder() -> Encoder { encoder }\n    func superEncoder(forKey key: K) -> Encoder { encoder }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/URLEncoder/ThrowingSingleValueContainer.swift",
    "content": "//\n//  File.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-11-14.\n//  \n\nimport Foundation\n\n// This simply throws an error as soon as you try to encode with it.\ninternal class ThrowingSingleValueContainer: SingleValueEncodingContainer {\n    let encoder: InternalURLQueryItemEncoder\n    let codingPath: [CodingKey] = []\n\n    init(encoder: InternalURLQueryItemEncoder) {\n        self.encoder = encoder\n    }\n\n    func encodeNil() throws {}\n\n    func encode(_ value: some Encodable) throws {\n        throw URLQueryItemEncoderError.singleValueContainerUnsupported\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/URLEncoder/ThrowingUnkeyedContainer.swift",
    "content": "//\n//  File.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-11-14.\n//  \n\nimport Foundation\n\n// This simply throws an error as soon as you try to encode with it.\ninternal class ThrowingUnkeyedContainer: UnkeyedEncodingContainer {\n    let encoder: any Encoder\n    let codingPath: [any CodingKey] = []\n    let count: Int = 0\n    \n    init(encoder: any Encoder) {\n        self.encoder = encoder\n    }\n    \n    func encodeNil() throws {}\n    \n    func encode(_ value: some Encodable) throws { throw URLQueryItemEncoderError.unkeyedContainerUnsupported }\n    \n    func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {\n        assertionFailure(\"We should throw an error *before* this gets called\")\n        return KeyedEncodingContainer(ThrowingKeyedContainer(encoder: encoder))\n    }\n    \n    func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer {\n        assertionFailure(\"We should throw an error *before* this gets called\")\n        return ThrowingUnkeyedContainer(encoder: encoder)\n    }\n    \n    func superEncoder() -> any Encoder { encoder }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/URLEncoder/TopLevelKeyedContainer.swift",
    "content": "//\n//  File.swift\n//  Rest\n//\n//  Created by Sjmarf on 2025-11-14.\n//  \n\nimport Foundation\n\ninternal class TopLevelKeyedContainer<K: CodingKey>: KeyedEncodingContainerProtocol {\n    var encoder: InternalURLQueryItemEncoder\n    var settings: URLQueryItemEncoderSettings\n\n    init(encoder: InternalURLQueryItemEncoder, settings: URLQueryItemEncoderSettings) {\n        self.encoder = encoder\n        self.settings = settings\n    }\n\n    var codingPath: [CodingKey] = []\n\n    func encodeNil(forKey key: K) throws {}\n\n    func encode(_ value: some Encodable, forKey key: K) throws {\n        let key = settings.convertToSnakeCase ? key.stringValue.camelToSnakeCase() : key.stringValue\n\n        if let valueString = convertValueToString(value) {\n            encoder.queryParams.append(.init(name: key, value: valueString))\n        } else {\n            let encoder = RetrievalEncoder(userInfo: self.encoder.userInfo)\n            try value.encode(to: encoder)\n            if let wrappedValue = encoder.encodedValue, let valueString = convertValueToString(wrappedValue) {\n                self.encoder.queryParams.append(.init(name: key, value: valueString))\n            } else {\n                throw URLQueryItemEncoderError.nestedContainersUnsupported\n            }\n        }\n    }\n    \n    func convertValueToString(_ value: any Encodable) -> String? {\n        if let value = value as? String {\n            value\n        } else if let value = value as? Int {\n            String(value)\n        } else if let value = value as? Double {\n            String(value)\n        } else if let value = value as? Bool {\n            value ? \"true\" : \"false\"\n        } else if let value = value as? URL {\n            value.absoluteString\n        } else {\n            nil\n        }\n    }\n\n    func nestedContainer<NestedKey>(\n        keyedBy type: NestedKey.Type, forKey key: K\n    ) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {\n        assertionFailure(\"We should throw an error *before* this gets called\")\n        return KeyedEncodingContainer(ThrowingKeyedContainer<NestedKey>(encoder: encoder))\n    }\n\n    func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer {\n        assertionFailure(\"We should throw an error *before* this gets called\")\n        return ThrowingUnkeyedContainer(encoder: encoder)\n    }\n\n    func superEncoder() -> Encoder { encoder }\n    func superEncoder(forKey key: K) -> Encoder { encoder }\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/URLEncoder/URLQueryItemEncoder.swift",
    "content": "//\n//  URLQueryItemEncoder.swift\n//  MlemMiddleware\n//\n//  Created by Sjmarf on 2025-02-20.\n//\n\nimport Foundation\n\npublic enum URLQueryItemEncoder {\n    public static func encode(\n        _ value: some Encodable,\n        convertToSnakeCase: Bool,\n        userInfo: [CodingUserInfoKey: Any] = [:]\n    ) throws(URLQueryItemEncoderError) -> [URLQueryItem] {\n        let encoder = InternalURLQueryItemEncoder(\n            userInfo: userInfo,\n            settings: .init(convertToSnakeCase: convertToSnakeCase)\n        )\n        do {\n            try value.encode(to: encoder)\n        } catch {\n            if let error = error as? URLQueryItemEncoderError {\n                throw error\n            }\n            assertionFailure()\n            throw .unknown\n        }\n        return encoder.queryParams\n    }\n}\n\npublic enum URLQueryItemEncoderError: Error {\n    case nestedContainersUnsupported\n    case singleValueContainerUnsupported\n    case unkeyedContainerUnsupported\n    case unknown // Should never be thrown\n}\n"
  },
  {
    "path": "Mlem/Packages/Rest/Sources/URLEncoder/URLQueryItemEncoderSettings.swift",
    "content": "//\n//  URLQueryItemEncoderSettings.swift\n//  Mlem\n//\n//  Created by Sjmarf on 2026-03-19.\n//\n\nstruct URLQueryItemEncoderSettings {\n    var convertToSnakeCase: Bool\n}\n"
  },
  {
    "path": "Mlem/Packages/Theming/Package.swift",
    "content": "// swift-tools-version: 6.2\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"Theming\",\n    platforms: [.iOS(.v18)],\n    products: [\n        // Products define the executables and libraries a package produces, making them visible to other packages.\n        .library(\n            name: \"Theming\",\n            targets: [\"Theming\"]\n        )\n    ],\n    dependencies: [],\n    targets: [\n        // Targets are the basic building blocks of a package, defining a module or a test suite.\n        // Targets can depend on other targets in this package and products from dependencies.\n        .target(\n            name: \"Theming\",\n            dependencies: []\n        )\n    ]\n)\n"
  },
  {
    "path": "Mlem/Packages/Theming/Sources/Theming/Color+Extensions.swift",
    "content": "//\n//  Color+Extensions.swift\n//  Mlem\n//\n//  Created by Eric Andrews on 2024-08-29.\n//\n\nimport Foundation\nimport SwiftUI\n\npublic extension Color {\n    init(light: UIColor, dark: UIColor) {\n        self.init(uiColor: UIColor { traits in\n            traits.userInterfaceStyle == .dark ? dark : light\n        })\n    }\n    \n    init(light: Color, dark: Color) {\n        self.init(uiColor: UIColor { traits in\n            traits.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light)\n        })\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Theming/Sources/Theming/EnvironmentValues+Extensions.swift",
    "content": "//\n//  File.swift\n//  Theming\n//\n//  Created by Sjmarf on 2025-03-06.\n//\n\nimport SwiftUI\n\npublic extension EnvironmentValues {\n    @Entry var palette: Palette = .default\n    @Entry var tint: ThemedColor = .themedAccent\n}\n\npublic extension View {\n    func palette(_ palette: Palette) -> some View {\n        environment(\\.palette, palette)\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Theming/Sources/Theming/Palette+Default.swift",
    "content": "//\n//  File.swift\n//  Theming\n//\n//  Created by Sjmarf on 2025-03-06.\n//\n\nimport SwiftUI\n\npublic extension Palette {\n    static let `default`: Self = .init(\n        bordered: false,\n        label: .init(\n            primary: .primary,\n            secondary: .secondary,\n            tertiary: .init(uiColor: .tertiaryLabel)\n        ),\n        background: .init(\n            primary: .init(uiColor: .systemBackground),\n            secondary: .init(uiColor: .secondarySystemBackground),\n            tertiary: .init(uiColor: .tertiarySystemBackground)\n        ),\n        groupedBackground: .init(\n            primary: .init(uiColor: .systemGroupedBackground),\n            secondary: .init(uiColor: .secondarySystemGroupedBackground),\n            tertiary: .init(uiColor: .tertiarySystemGroupedBackground)\n        ),\n        thumbnailBackground: Color(UIColor.systemGray4),\n        contrastingLabel: .white,\n        accent: .blue,\n        neutralAccent: .gray,\n        colorfulAccents: [.orange, .pink, .blue, .green, .purple, .indigo, .mint, .teal, .yellow],\n        commentIndentColors: [.red, .orange, .yellow, .green, .blue, .purple],\n        accountAgeColors: [\n            .green,\n            .init(\n                // This is `.green.mix(with: .cyan, by: 0.333)`\n                light: .init(red: 0.20605278, green: 0.7933883, blue: 0.53997606, alpha: 1.0),\n                dark: .init(red: 0.23807898, green: 0.8318233, blue: 0.56663805, alpha: 1.0)\n            ),\n            .init(\n                // This is `.green.mix(with: .cyan, by: 0.666)`\n                light: .init(red: 0.2665288, green: 0.7745191, blue: 0.7497066, alpha: 1.0),\n                dark: .init(red: 0.2917948, green: 0.8128531, blue: 0.7726594, alpha: 1.0)\n            ),\n            .cyan,\n            .brown\n        ],\n        positive: .green,\n        negative: .red,\n        warning: .red,\n        caution: .orange,\n        upvote: .blue,\n        downvote: .red,\n        save: .green,\n        read: .purple,\n        favorite: .blue,\n        administration: .teal,\n        moderation: .cyan,\n        federatedFeed: .blue,\n        localFeed: .purple,\n        subscribedFeed: .red,\n        moderatedFeed: .cyan,\n        savedFeed: .green,\n        popularFeed: .indigo,\n        suggestedFeed: .orange,\n        inbox: .purple,\n        fediseerEndorsement: .cyan\n    )\n}\n"
  },
  {
    "path": "Mlem/Packages/Theming/Sources/Theming/Palette.swift",
    "content": "//\n//  Palette.swift\n//  Theming\n//\n//  Created by Sjmarf on 2025-03-06.\n//\n\nimport SwiftUI\n\n@preconcurrency\npublic struct Palette {\n    public var bordered: Bool\n    \n    public var label: ColorHierarchy\n    public var background: ColorHierarchy\n    public var groupedBackground: ColorHierarchy\n    \n    public var thumbnailBackground: Color\n    public var contrastingLabel: Color\n    \n    public var accent: Color\n    public var neutralAccent: Color\n    public var colorfulAccents: [Color]\n    public var commentIndentColors: [Color]\n    public var accountAgeColors: [Color]\n    \n    public var positive: Color\n    public var negative: Color\n    public var warning: Color\n    public var caution: Color\n    \n    public var upvote: Color\n    public var downvote: Color\n    public var save: Color\n    public var read: Color\n    public var favorite: Color\n    public var administration: Color\n    public var moderation: Color\n    \n    public var federatedFeed: Color\n    public var localFeed: Color\n    public var subscribedFeed: Color\n    public var moderatedFeed: Color\n    public var savedFeed: Color\n    public var popularFeed: Color\n    public var suggestedFeed: Color\n    public var inbox: Color\n\n    public var fediseerEndorsement: Color\n    public var fediseerHesitation: Color { caution }\n    public var fediseerCensure: Color { warning }\n    \n    public init(\n        bordered: Bool,\n        label: ColorHierarchy,\n        background: ColorHierarchy,\n        groupedBackground: ColorHierarchy,\n        thumbnailBackground: Color,\n        contrastingLabel: Color,\n        accent: Color,\n        neutralAccent: Color,\n        colorfulAccents: [Color],\n        commentIndentColors: [Color],\n        accountAgeColors: [Color],\n        positive: Color,\n        negative: Color,\n        warning: Color,\n        caution: Color,\n        upvote: Color,\n        downvote: Color,\n        save: Color,\n        read: Color,\n        favorite: Color,\n        administration: Color,\n        moderation: Color,\n        federatedFeed: Color,\n        localFeed: Color,\n        subscribedFeed: Color,\n        moderatedFeed: Color,\n        savedFeed: Color,\n        popularFeed: Color,\n        suggestedFeed: Color,\n        inbox: Color,\n        fediseerEndorsement: Color\n    ) {\n        self.bordered = bordered\n        self.label = label\n        self.background = background\n        self.groupedBackground = groupedBackground\n        self.thumbnailBackground = thumbnailBackground\n        self.contrastingLabel = contrastingLabel\n        self.accent = accent\n        self.neutralAccent = neutralAccent\n        self.colorfulAccents = colorfulAccents\n        self.commentIndentColors = commentIndentColors\n        self.accountAgeColors = accountAgeColors\n        self.positive = positive\n        self.negative = negative\n        self.warning = warning\n        self.caution = caution\n        self.upvote = upvote\n        self.downvote = downvote\n        self.save = save\n        self.read = read\n        self.favorite = favorite\n        self.administration = administration\n        self.moderation = moderation\n        self.federatedFeed = federatedFeed\n        self.localFeed = localFeed\n        self.subscribedFeed = subscribedFeed\n        self.moderatedFeed = moderatedFeed\n        self.savedFeed = savedFeed\n        self.popularFeed = popularFeed\n        self.suggestedFeed = suggestedFeed\n        self.inbox = inbox\n        self.fediseerEndorsement = fediseerEndorsement\n    }\n}\n\npublic extension Palette {\n    struct ColorHierarchy {\n        public var primary: Color\n        public var secondary: Color\n        public var tertiary: Color\n        \n        public init(primary: Color, secondary: Color, tertiary: Color) {\n            self.primary = primary\n            self.secondary = secondary\n            self.tertiary = tertiary\n        }\n    }\n}\n"
  },
  {
    "path": "Mlem/Packages/Theming/Sources/Theming/ThemedColor.swift",
    "content": "//\n//  ThemedColor.swift\n//  Theming\n//\n//  Created by Sjmarf on 2025-03-06.\n//\n\nimport Foundation\nimport SwiftUI\n\npublic struct ThemedColor: ShapeStyle, Hashable, View, Sendable {\n    fileprivate let hashString: String\n    \n    let getColor: @Sendable (Palette) -> Color\n    var opacity: CGFloat = 1\n    \n    public func body(palette: Palette) -> some View {\n        resolve(with: palette)\n    }\n    \n    public func gradient(palette: Palette) -> AnyGradient {\n        resolve(with: palette).gradient\n    }\n\n    public nonisolated func resolve(in environment: EnvironmentValues) -> Color {\n        resolve(with: environment.palette)\n    }\n    \n    public func resolve(with palette: Palette) -> Color {\n        getColor(palette).opacity(opacity)\n    }\n    \n    public func opacity(_ newOpacity: CGFloat) -> ThemedColor {\n        .init(hashString: hashString, getColor: getColor, opacity: newOpacity)\n    }\n    \n    public nonisolated func hash(into hasher: inout Hasher) {\n        hasher.combine(hashString)\n        hasher.combine(opacity)\n    }\n    \n    public nonisolated static func == (lhs: Self, rhs: Self) -> Bool {\n        lhs.hashValue == rhs.hashValue\n    }\n}\n\npublic extension ShapeStyle where Self == ThemedColor {\n    static var themedPrimary: ThemedColor {\n        .init(hashString: \"primary\", getColor: \\.label.primary)\n    }\n\n    static var themedSecondary: ThemedColor {\n        .init(hashString: \"secondary\", getColor: \\.label.secondary)\n    }\n\n    static var themedTertiary: ThemedColor {\n        .init(hashString: \"tertiary\", getColor: \\.label.tertiary)\n    }\n    \n    static var themedBackground: ThemedColor {\n        .init(hashString: \"background\", getColor: \\.background.primary)\n    }\n\n    static var themedSecondaryBackground: ThemedColor {\n        .init(hashString: \"secondaryBackground\", getColor: \\.background.secondary)\n    }\n\n    static var themedTertiaryBackground: ThemedColor {\n        .init(hashString: \"tertiaryBackground\", getColor: \\.background.tertiary)\n    }\n    \n    static var themedGroupedBackground: ThemedColor {\n        .init(hashString: \"groupedBackground\", getColor: \\.groupedBackground.primary)\n    }\n\n    static var themedSecondaryGroupedBackground: ThemedColor {\n        .init(hashString: \"secondaryGroupedBackground\", getColor: \\.groupedBackground.secondary)\n    }\n\n    static var themedTertiaryGroupedBackground: ThemedColor {\n        .init(hashString: \"tertiaryGroupedBackground\", getColor: \\.groupedBackground.tertiary)\n    }\n    \n    static var themedContrastingLabel: ThemedColor {\n        .init(hashString: \"contrastingLabel\", getColor: \\.contrastingLabel)\n    }\n\n    static var themedThumbnailBackground: ThemedColor {\n        .init(hashString: \"thumbnailBackground\", getColor: \\.thumbnailBackground)\n    }\n    \n    static var themedAccent: ThemedColor {\n        .init(hashString: \"accent\", getColor: \\.accent)\n    }\n\n    static var themedNeutralAccent: ThemedColor {\n        .init(hashString: \"neutralAccent\", getColor: \\.neutralAccent)\n    }\n    \n    static func themedColorfulAccent(_ index: Int) -> ThemedColor {\n        .init(hashString: \"colorfulAccent\\(index)\") { $0.colorfulAccents[index % $0.colorfulAccents.count] }\n    }\n    \n    static func themedCommentIndentColor(_ index: Int) -> ThemedColor {\n        .init(hashString: \"commentIndentColor\\(index)\") { $0.commentIndentColors[index % $0.commentIndentColors.count] }\n    }\n\n    static func themedAccountAgeColor(_ index: Int) -> ThemedColor {\n        .init(hashString: \"accountAgeColor\\(index)\") { $0.accountAgeColors[min(index, $0.accountAgeColors.count - 1)] }\n    }\n\n    static var themedPositive: ThemedColor {\n        .init(hashString: \"positive\", getColor: \\.positive)\n    }\n\n    static var themedNegative: ThemedColor {\n        .init(hashString: \"negative\", getColor: \\.negative)\n    }\n\n    static var themedWarning: ThemedColor {\n        .init(hashString: \"warning\", getColor: \\.warning)\n    }\n\n    static var themedCaution: ThemedColor {\n        .init(hashString: \"caution\", getColor: \\.caution)\n    }\n\n    static var themedUpvote: ThemedColor {\n        .init(hashString: \"upvote\", getColor: \\.upvote)\n    }\n\n    static var themedDownvote: ThemedColor {\n        .init(hashString: \"downvote\", getColor: \\.downvote)\n    }\n\n    static var themedSave: ThemedColor {\n        .init(hashString: \"save\", getColor: \\.save)\n    }\n\n    static var themedRead: ThemedColor {\n        .init(hashString: \"read\", getColor: \\.read)\n    }\n\n    static var themedFavorite: ThemedColor {\n        .init(hashString: \"favorite\", getColor: \\.favorite)\n    }\n\n    static var themedAdministration: ThemedColor {\n        .init(hashString: \"administration\", getColor: \\.administration)\n    }\n\n    static var themedModeration: ThemedColor {\n        .init(hashString: \"moderation\", getColor: \\.moderation)\n    }\n    \n    static var themedFederatedFeed: ThemedColor {\n        .init(hashString: \"federatedFeed\", getColor: \\.federatedFeed)\n    }\n\n    static var themedLocalFeed: ThemedColor {\n        .init(hashString: \"localFeed\", getColor: \\.localFeed)\n    }\n\n    static var themedSubscribedFeed: ThemedColor {\n        .init(hashString: \"subscribedFeed\", getColor: \\.subscribedFeed)\n    }\n\n    static var themedModeratedFeed: ThemedColor {\n        .init(hashString: \"moderatedFeed\", getColor: \\.moderatedFeed)\n    }\n\n    static var themedSavedFeed: ThemedColor {\n        .init(hashString: \"savedFeed\", getColor: \\.savedFeed)\n    }\n\n    static var themedPopularFeed: ThemedColor {\n        .init(hashString: \"popularFeed\", getColor: \\.popularFeed)\n    }\n\n    static var themedSuggestedFeed: ThemedColor {\n        .init(hashString: \"suggestedFeed\", getColor: \\.suggestedFeed)\n    }\n\n    static var themedInbox: ThemedColor {\n        .init(hashString: \"inbox\", getColor: \\.inbox)\n    }\n\n    static var themedFediseerEndorsement: ThemedColor {\n        .init(hashString: \"fediseerEndorsement\", getColor: \\.fediseerEndorsement)\n    }\n\n    static var themedFediseerHesitation: ThemedColor {\n        .init(hashString: \"fediseerHesitation\", getColor: \\.fediseerHesitation)\n    }\n\n    static var themedFediseerCensure: ThemedColor {\n        .init(hashString: \"fediseerCensure\", getColor: \\.fediseerCensure)\n    }\n\n    static var themedCommentAccent: ThemedColor { themedColorfulAccent(0) }\n    static var themedPostAccent: ThemedColor { themedColorfulAccent(1) }\n    static var themedPersonAccent: ThemedColor { themedColorfulAccent(2) }\n    static var themedCommunityAccent: ThemedColor { themedColorfulAccent(3) }\n    static var themedLockAccent: ThemedColor { themedColorfulAccent(0) }\n    \n    static var themedDivider: ThemedColor {\n        .init(hashString: \"divider\") {\n            Color(light: $0.label.secondary.opacity(0.5), dark: $0.neutralAccent.opacity(0.35))\n        }\n    }\n    \n    @_disfavoredOverload\n    static var clear: ThemedColor { .init(hashString: \"clear\", getColor: { _ in .clear }) }\n}\n"
  },
  {
    "path": "Mlem/Packages/Theming/Sources/Theming/View+Tint.swift",
    "content": "//\n//  File.swift\n//  Theming\n//\n//  Created by Sjmarf on 2025-03-07.\n//\n\nimport SwiftUI\n\nprivate struct ThemedTintViewModifier: ViewModifier {\n    @Environment(\\.palette) private var palette\n    \n    let themedColor: ThemedColor\n    \n    func body(content: Content) -> some View {\n        content\n            .tint(themedColor.resolve(with: palette))\n            .environment(\\.tint, themedColor)\n    }\n}\n\npublic extension View {\n    @ViewBuilder\n    func tint(_ themedColor: ThemedColor?) -> some View {\n        if let themedColor {\n            modifier(ThemedTintViewModifier(themedColor: themedColor))\n        } else {\n            self\n        }\n    }\n}\n\nprivate struct ThemedGradientTintModifier: ViewModifier {\n    @Environment(\\.palette) private var palette\n    \n    let themedColor: ThemedColor\n    \n    func body(content: Content) -> some View {\n        content\n            .tint(themedColor.gradient(palette: palette))\n    }\n}\n\npublic extension View {\n    func gradientTint(_ themedColor: ThemedColor) -> some View {\n        modifier(ThemedGradientTintModifier(themedColor: themedColor))\n            .environment(\\.tint, themedColor)\n    }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"generate-swift-asset-symbol-extensions\" : \"disabled\"\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/image.droplets.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"kenrick-mills-uxk0JKMrZts-unsplash.jpg\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/image.meguro_river.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"photo-1524413840807-0c3cb6fa808d.jpg\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/image.yorkshire_dales.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"620px-2015_Swaledale_from_Kisdon_Hill.jpg\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/pfp.balloon.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Screenshot 2025-02-04 at 20.39.06.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/pfp.circuit.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Screenshot 2025-02-04 at 20.56.12.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/pfp.firework.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Screenshot 2025-02-02 at 17.55.51.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/pfp.fish.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"fish-silly-fish.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/pfp.flowers.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Screenshot 2025-02-02 at 20.56.36.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/pfp.goose.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Screenshot 2025-02-02 at 21.16.20.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/pfp.lakeside.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Screenshot 2025-02-04 at 20.38.34.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/pfp.news.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"406d3ede-5b39-493d-98c0-6ddab03a59aa.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/pfp.person.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"icons8-person-64.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/Preview Assets.xcassets/pfp.shower.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"image_proxy.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Mlem/Preview Content/PreviewLocalizable.xcstrings",
    "content": "{\n  \"sourceLanguage\" : \"en\",\n  \"strings\" : {\n    \"community.1.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"World News\"\n          }\n        }\n      }\n    },\n    \"community.1.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"news\"\n          }\n        }\n      }\n    },\n    \"community.2.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Pics\"\n          }\n        }\n      }\n    },\n    \"community.2.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"pics\"\n          }\n        }\n      }\n    },\n    \"community.3.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"me_irl\"\n          }\n        }\n      }\n    },\n    \"community.3.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"me_irl\"\n          }\n        }\n      }\n    },\n    \"community.4.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Technology\"\n          }\n        }\n      }\n    },\n    \"community.4.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"technology\"\n          }\n        }\n      }\n    },\n    \"community.5.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Nature\"\n          }\n        }\n      }\n    },\n    \"community.5.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"nature\"\n          }\n        }\n      }\n    },\n    \"community.6.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Nature\"\n          }\n        }\n      }\n    },\n    \"community.6.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"showerthoughts\"\n          }\n        }\n      }\n    },\n    \"person.1.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Flowertail\"\n          }\n        }\n      }\n    },\n    \"person.1.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"flowertail\"\n          }\n        }\n      }\n    },\n    \"person.2.description\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"HONK\"\n          }\n        }\n      }\n    },\n    \"person.2.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Commander Goose\"\n          }\n        }\n      }\n    },\n    \"person.2.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"CommanderGoose\"\n          }\n        }\n      }\n    },\n    \"person.3.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"BillyDAFISH\"\n          }\n        }\n      }\n    },\n    \"person.3.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"BillyDAFISH\"\n          }\n        }\n      }\n    },\n    \"person.4.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Grt38\"\n          }\n        }\n      }\n    },\n    \"person.4.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Grt38\"\n          }\n        }\n      }\n    },\n    \"person.5.displayName\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"AnteSocial\"\n          }\n        }\n      }\n    },\n    \"person.5.name\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"ante_social_58\"\n          }\n        }\n      }\n    },\n    \"post.1.title\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"The Yorkshire Dales, England\"\n          }\n        }\n      }\n    },\n    \"post.2.title\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"Meguro River, Matsuno, Japan\"\n          }\n        }\n      }\n    },\n    \"post.3.title\" : {\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"During a nuclear explosion, there is a certain distance of the radius where all the frozen supermarket pizzas are cooked to perfection.\"\n          }\n        }\n      }\n    }\n  },\n  \"version\" : \"1.0\"\n}"
  },
  {
    "path": "Mlem/Settings.bundle/Root.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>StringsTable</key>\n\t<string>Root</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "Mlem.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 70;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t0300309D2C4163C9009A65FF /* CommentTreeTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300309C2C4163C9009A65FF /* CommentTreeTracker.swift */; };\n\t\t030030A12C416B0B009A65FF /* RefreshPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030030A02C416B0B009A65FF /* RefreshPopupView.swift */; };\n\t\t030050D32D109B7E002B1E99 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030050D22D109B7E002B1E99 /* ReportView.swift */; };\n\t\t030050D52D10AE30002B1E99 /* Report+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030050D42D10AE30002B1E99 /* Report+Extensions.swift */; };\n\t\t030056A42D7DB05A00EB0BA3 /* SharingLinksSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030056A32D7DB05A00EB0BA3 /* SharingLinksSettingsView.swift */; };\n\t\t030056A62D7DBD4F00EB0BA3 /* Sharable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030056A52D7DBD4F00EB0BA3 /* Sharable+Extensions.swift */; };\n\t\t030056BB2D7E137800EB0BA3 /* ShareInstancePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030056BA2D7E137800EB0BA3 /* ShareInstancePickerView.swift */; };\n\t\t0302A8802F9BC379000A127A /* SearchHomeCategoryLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0302A87F2F9BC379000A127A /* SearchHomeCategoryLabelStyle.swift */; };\n\t\t03036C742C71408700C6DA1D /* CounterAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03036C732C71408700C6DA1D /* CounterAppearance.swift */; };\n\t\t03036C832C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03036C822C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift */; };\n\t\t03049A1A2C6502F300FF6889 /* FormSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03049A192C6502F300FF6889 /* FormSection.swift */; };\n\t\t03049A1C2C65039400FF6889 /* ActiveUserCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03049A1B2C65039400FF6889 /* ActiveUserCountView.swift */; };\n\t\t03049A1E2C6508F400FF6889 /* RegistrationMode+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03049A1D2C6508F400FF6889 /* RegistrationMode+Extensions.swift */; };\n\t\t03049A202C650A8100FF6889 /* FormReadout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03049A1F2C650A8100FF6889 /* FormReadout.swift */; };\n\t\t03049A222C650B2C00FF6889 /* CommunityDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03049A212C650B2C00FF6889 /* CommunityDetailsView.swift */; };\n\t\t0305EBAA2D32B3B80066E5AD /* ModlogView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0305EBA92D32B3B80066E5AD /* ModlogView+Logic.swift */; };\n\t\t0305EBAC2D32C9300066E5AD /* ModlogEntryType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0305EBAB2D32C9300066E5AD /* ModlogEntryType+Extensions.swift */; };\n\t\t0305EBB22D35C1B70066E5AD /* RegistrationApplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0305EBB12D35C1B70066E5AD /* RegistrationApplicationView.swift */; };\n\t\t030778EC2C52ED350018E61C /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 030778EB2C52ED350018E61C /* Localizable.xcstrings */; };\n\t\t030BCB1B2C3EA5FD0037680F /* InstanceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030BCB1A2C3EA5FD0037680F /* InstanceDetailsView.swift */; };\n\t\t030E95E72C80A20A0045BC2C /* View+NavigationTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E95E62C80A20A0045BC2C /* View+NavigationTransition.swift */; };\n\t\t030EE3042D651A4100D58C2C /* View+Refreshable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030EE3032D651A4100D58C2C /* View+Refreshable.swift */; };\n\t\t030FF6792BC84F7E00F6BFAC /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 030FF6782BC84F7E00F6BFAC /* SwiftUIIntrospect */; };\n\t\t030FF67B2BC8521600F6BFAC /* CustomTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FF67A2BC8521600F6BFAC /* CustomTabView.swift */; };\n\t\t030FF67D2BC8524500F6BFAC /* CustomTabItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FF67C2BC8524500F6BFAC /* CustomTabItem.swift */; };\n\t\t030FF67F2BC8544700F6BFAC /* CustomTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FF67E2BC8544600F6BFAC /* CustomTabBarController.swift */; };\n\t\t030FF6812BC859FD00F6BFAC /* CustomTabViewHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FF6802BC859FD00F6BFAC /* CustomTabViewHostingController.swift */; };\n\t\t0311ADB72E4DF49800EC3120 /* SearchHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0311ADB62E4DF49800EC3120 /* SearchHomeView.swift */; };\n\t\t0311ADB92E4E0E0800EC3120 /* VisitAgainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0311ADB82E4E0E0800EC3120 /* VisitAgainView.swift */; };\n\t\t0311ADBD2E4F668900EC3120 /* TopCommunitiesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0311ADBC2E4F668900EC3120 /* TopCommunitiesListView.swift */; };\n\t\t0311ADBF2E4F68D000EC3120 /* TopPeopleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0311ADBE2E4F68D000EC3120 /* TopPeopleListView.swift */; };\n\t\t0311ADC12E4F693B00EC3120 /* TopInstancesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0311ADC02E4F693B00EC3120 /* TopInstancesListView.swift */; };\n\t\t03134A502BEAD245002662CC /* NavigationLink+NavigationPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03134A4F2BEAD245002662CC /* NavigationLink+NavigationPage.swift */; };\n\t\t03134A522BEAD69F002662CC /* SettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03134A512BEAD69F002662CC /* SettingsPage.swift */; };\n\t\t03134A582BEC1C46002662CC /* AccountListSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03134A572BEC1C46002662CC /* AccountListSettingsView.swift */; };\n\t\t03134A5A2BEC2253002662CC /* AvatarStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03134A592BEC2253002662CC /* AvatarStackView.swift */; };\n\t\t0315B1BE2C74C3D6006D4F82 /* CommentEditorView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315B1BD2C74C3D6006D4F82 /* CommentEditorView+Logic.swift */; };\n\t\t0315B1C12C74C71A006D4F82 /* CommentEditorView+Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315B1C02C74C71A006D4F82 /* CommentEditorView+Context.swift */; };\n\t\t0315B1C62C754802006D4F82 /* PostEditorView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315B1C52C754802006D4F82 /* PostEditorView+Logic.swift */; };\n\t\t0316CD642C382A6A009EA8EA /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0316CD632C382A6A009EA8EA /* MessageView.swift */; };\n\t\t0318BA9F2D72405F006CA71F /* PostSortType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0318BA9E2D72405F006CA71F /* PostSortType+Extensions.swift */; };\n\t\t031CA0D92E4FBFD800CF0C0F /* MarkAllAsReadButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031CA0D82E4FBFD800CF0C0F /* MarkAllAsReadButton.swift */; };\n\t\t031CA4AE2E58A84E00CF0C0F /* View+WithSheetSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031CA4AD2E58A84E00CF0C0F /* View+WithSheetSearch.swift */; };\n\t\t031CA5752E5900C800CF0C0F /* SwipeConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031CA5742E5900C800CF0C0F /* SwipeConfiguration+Extensions.swift */; };\n\t\t031CA5772E5900E900CF0C0F /* QuickSwipes in Frameworks */ = {isa = PBXBuildFile; productRef = 031CA5762E5900E900CF0C0F /* QuickSwipes */; };\n\t\t031CA5B52E590B0D00CF0C0F /* QuickSwipeAction+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031CA5B42E590B0D00CF0C0F /* QuickSwipeAction+Extensions.swift */; };\n\t\t031CA5B72E599F7E00CF0C0F /* View+QuickSwipes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031CA5B62E599F7E00CF0C0F /* View+QuickSwipes.swift */; };\n\t\t031DBA682F9A65DC00B4BAE4 /* BackendClient+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031DBA672F9A65DC00B4BAE4 /* BackendClient+Extensions.swift */; };\n\t\t031DBA772F9A93AD00B4BAE4 /* EventRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031DBA762F9A93AD00B4BAE4 /* EventRowView.swift */; };\n\t\t031DBA792F9A95B200B4BAE4 /* SearchHomeLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031DBA782F9A95B200B4BAE4 /* SearchHomeLabelStyle.swift */; };\n\t\t031DBA7B2F9A993000B4BAE4 /* SearchHomeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031DBA7A2F9A993000B4BAE4 /* SearchHomeListView.swift */; };\n\t\t031E2D512BEF961D0003BC45 /* SubscriptionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2D502BEF961D0003BC45 /* SubscriptionListView.swift */; };\n\t\t031E2D5B2BEFC9460003BC45 /* ThemeSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2D5A2BEFC9460003BC45 /* ThemeSettingsView.swift */; };\n\t\t031E2D5D2BEFCC630003BC45 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2D5C2BEFCC630003BC45 /* SettingsView.swift */; };\n\t\t031EC5302E5F77D7003408B7 /* FeedContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031EC52F2E5F77D7003408B7 /* FeedContext.swift */; };\n\t\t0320B64F2C8A638A00D38548 /* SignUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0320B64E2C8A638A00D38548 /* SignUpView.swift */; };\n\t\t0320B6542C8B65EB00D38548 /* Captcha+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0320B6532C8B65EB00D38548 /* Captcha+Extensions.swift */; };\n\t\t0320B6582C8BB3C400D38548 /* SignUpView+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0320B6572C8BB3C400D38548 /* SignUpView+Views.swift */; };\n\t\t0320B65A2C8CBB0D00D38548 /* SignUpView+EmailConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0320B6592C8CBB0D00D38548 /* SignUpView+EmailConfirmationView.swift */; };\n\t\t0320B65C2C8CFBDC00D38548 /* ImageUploadHistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0320B65B2C8CFBDC00D38548 /* ImageUploadHistoryManager.swift */; };\n\t\t0320B6612C8DFCF100D38548 /* SearchView+FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0320B6602C8DFCF100D38548 /* SearchView+FiltersView.swift */; };\n\t\t0320B6632C8F8D5A00D38548 /* InstanceSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0320B6622C8F8D5A00D38548 /* InstanceSort.swift */; };\n\t\t0320B6652C91DBD500D38548 /* NavigationPage+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0320B6642C91DBD500D38548 /* NavigationPage+View.swift */; };\n\t\t0320B6672C93504600D38548 /* SignUpView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0320B6662C93504600D38548 /* SignUpView+Logic.swift */; };\n\t\t0320B6692C93506300D38548 /* SearchView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0320B6682C93506300D38548 /* SearchView+Logic.swift */; };\n\t\t0324FA772C1F0AE100F6247D /* Readout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0324FA762C1F0AE100F6247D /* Readout.swift */; };\n\t\t0324FA7B2C1F2CD200F6247D /* InfoStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0324FA7A2C1F2CD200F6247D /* InfoStackView.swift */; };\n\t\t0325B93A2D3A9E8100E28B97 /* InboxBadgeSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0325B9392D3A9E8100E28B97 /* InboxBadgeSettingsView.swift */; };\n\t\t0325B93C2D3AA62500E28B97 /* InboxItemType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0325B93B2D3AA62500E28B97 /* InboxItemType+Extensions.swift */; };\n\t\t0325B93E2D3AAE9E00E28B97 /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0325B93D2D3AAE9E00E28B97 /* SettingsHeaderView.swift */; };\n\t\t03267D822BED489C009D6268 /* AvatarBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03267D812BED489C009D6268 /* AvatarBannerView.swift */; };\n\t\t03267D842BED49CE009D6268 /* AccountSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03267D832BED49CE009D6268 /* AccountSettingsView.swift */; };\n\t\t032A22012EB2A4D100E8B346 /* PersonContentFeedLoader+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032A22002EB2A4D100E8B346 /* PersonContentFeedLoader+Extensions.swift */; };\n\t\t032C32042C3439C600595286 /* ActorIdentifiable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C32032C3439C600595286 /* ActorIdentifiable+Extensions.swift */; };\n\t\t032C32082C34469900595286 /* SelectableContentProviding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C32072C34469900595286 /* SelectableContentProviding+Extensions.swift */; };\n\t\t032C320A2C34495D00595286 /* SelectTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C32092C34495D00595286 /* SelectTextView.swift */; };\n\t\t032C32162C36F65500595286 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C32152C36F65500595286 /* ReplyView.swift */; };\n\t\t032C32182C36F70300595286 /* ReplyBarConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C32172C36F70300595286 /* ReplyBarConfiguration.swift */; };\n\t\t0331715E2CCD6D95002DA370 /* ContentPurgeEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0331715D2CCD6D95002DA370 /* ContentPurgeEditorView.swift */; };\n\t\t033171782CCE89E3002DA370 /* PurgableProviding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033171772CCE89E3002DA370 /* PurgableProviding+Extensions.swift */; };\n\t\t0335AE112D8991330094FFD9 /* View+HiddenNavigationTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0335AE102D8991330094FFD9 /* View+HiddenNavigationTitle.swift */; };\n\t\t033819282D4424D9000AFC55 /* SafetyWarningsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033819272D4424D9000AFC55 /* SafetyWarningsSettingsView.swift */; };\n\t\t033EF4102CB9AEF7004D8A3F /* ExpandedPostView+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033EF40F2CB9AEF7004D8A3F /* ExpandedPostView+Views.swift */; };\n\t\t033F84492D18D1F400D87A9E /* MessageFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84482D18D1F400D87A9E /* MessageFeedView.swift */; };\n\t\t033F844D2D18D90900D87A9E /* MessageBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F844C2D18D90900D87A9E /* MessageBubbleView.swift */; };\n\t\t033F84512D196AFD00D87A9E /* MessageFeedView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84502D196AFD00D87A9E /* MessageFeedView+Logic.swift */; };\n\t\t033F84662D1C780900D87A9E /* ModlogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84652D1C780900D87A9E /* ModlogButtonView.swift */; };\n\t\t033F84732D1C784600D87A9E /* ModlogEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84702D1C784600D87A9E /* ModlogEntryView.swift */; };\n\t\t033F84742D1C784600D87A9E /* ModlogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84712D1C784600D87A9E /* ModlogView.swift */; };\n\t\t033F84782D1D68D200D87A9E /* ModlogEntryContent+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84772D1D68D200D87A9E /* ModlogEntryContent+Extensions.swift */; };\n\t\t033F84AD2C298466002E3EDF /* SectionIndexTitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84AC2C298466002E3EDF /* SectionIndexTitles.swift */; };\n\t\t033F84B12C29907F002E3EDF /* FeedbackType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84B02C29907F002E3EDF /* FeedbackType.swift */; };\n\t\t033F84BB2C2ACB96002E3EDF /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84BA2C2ACB96002E3EDF /* CommentView.swift */; };\n\t\t033F84BD2C2ACC5F002E3EDF /* CommentBarConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84BC2C2ACC5F002E3EDF /* CommentBarConfiguration.swift */; };\n\t\t033F84C12C2AD072002E3EDF /* CommentTreeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84C02C2AD072002E3EDF /* CommentTreeNode.swift */; };\n\t\t033F84C32C2B12AA002E3EDF /* InstanceSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84C22C2B12AA002E3EDF /* InstanceSummary.swift */; };\n\t\t033F84C82C2B193D002E3EDF /* MlemStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84C72C2B193D002E3EDF /* MlemStats.swift */; };\n\t\t033F84CC2C2B1E46002E3EDF /* HandleThreadiverseLinksModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F84CB2C2B1E46002E3EDF /* HandleThreadiverseLinksModifier.swift */; };\n\t\t033F84D92C2B61FB002E3EDF /* ToastType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03500C212BF5594100CAA076 /* ToastType.swift */; };\n\t\t033FCAEC2C57DCCD007B7CD1 /* ListingType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCAEB2C57DCCD007B7CD1 /* ListingType+Extensions.swift */; };\n\t\t033FCAEE2C57DD14007B7CD1 /* CaptchaDifficulty+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCAED2C57DD14007B7CD1 /* CaptchaDifficulty+Extensions.swift */; };\n\t\t033FCAF42C59843E007B7CD1 /* CommunityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCAF32C59843E007B7CD1 /* CommunityView.swift */; };\n\t\t033FCB272C5E3933007B7CD1 /* AlternateIconLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCB242C5E3933007B7CD1 /* AlternateIconLabel.swift */; };\n\t\t033FCB282C5E3933007B7CD1 /* AlternateIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCB222C5E3933007B7CD1 /* AlternateIcon.swift */; };\n\t\t033FCB292C5E3933007B7CD1 /* IconSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCB252C5E3933007B7CD1 /* IconSettingsView.swift */; };\n\t\t033FCB2A2C5E3933007B7CD1 /* AlternateIconCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCB232C5E3933007B7CD1 /* AlternateIconCell.swift */; };\n\t\t033FCB3E2C5E7FA9007B7CD1 /* View+OutdatedFeedPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCB3D2C5E7FA9007B7CD1 /* View+OutdatedFeedPopup.swift */; };\n\t\t034065C32D83742900637308 /* View+NavigtionStackPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034065C22D83742900637308 /* View+NavigtionStackPreview.swift */; };\n\t\t034147FD2D8F5844005503AF /* ExpandedPostHistoryTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034147FC2D8F5844005503AF /* ExpandedPostHistoryTracker.swift */; };\n\t\t0341480D2D8F63A6005503AF /* MlemMiddleware in Frameworks */ = {isa = PBXBuildFile; productRef = 0341480C2D8F63A6005503AF /* MlemMiddleware */; };\n\t\t0343C0482D3AD6DB001CF709 /* Set+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0343C0472D3AD6DB001CF709 /* Set+Extensions.swift */; };\n\t\t034690932D0F4D720073E664 /* RemovableProviding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034690922D0F4D720073E664 /* RemovableProviding+Extensions.swift */; };\n\t\t034690992D105DFD0073E664 /* InboxView+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034690982D105DFD0073E664 /* InboxView+Types.swift */; };\n\t\t0347A6FB2F97F4CF00EFD670 /* FediverseEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 0347A6FA2F97F4CF00EFD670 /* FediverseEvents */; };\n\t\t0348F98D2DDBB526006639CD /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0348F98C2DDBB526006639CD /* OnboardingView.swift */; };\n\t\t034A82032EBA688F00E5F904 /* LinkEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034A82022EBA688F00E5F904 /* LinkEditorView.swift */; };\n\t\t034A85712EC0A1FA00E5F904 /* InstanceCommunityListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034A85702EC0A1FA00E5F904 /* InstanceCommunityListView.swift */; };\n\t\t034B947F2C091EDD00039AF4 /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034B947E2C091EDD00039AF4 /* ProfileHeaderView.swift */; };\n\t\t034B94812C09306D00039AF4 /* CommunityOrPersonStub+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034B94802C09306D00039AF4 /* CommunityOrPersonStub+Extensions.swift */; };\n\t\t034B94832C09340A00039AF4 /* MarkdownConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034B94822C09340A00039AF4 /* MarkdownConfiguration+Extensions.swift */; };\n\t\t034B94892C09360A00039AF4 /* Int+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034B94882C09360A00039AF4 /* Int+Extensions.swift */; };\n\t\t034B948E2C0937BA00039AF4 /* FancyScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034B948D2C0937BA00039AF4 /* FancyScrollView.swift */; };\n\t\t034CC0302D22C5BE00C557D3 /* WarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034CC02F2D22C5BE00C557D3 /* WarningOverlayView.swift */; };\n\t\t03500C242BF55D0E00CAA076 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03500C232BF55D0E00CAA076 /* Toast.swift */; };\n\t\t03500C272BF69D1D00CAA076 /* ToastModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03500C262BF69D1D00CAA076 /* ToastModel.swift */; };\n\t\t03500C2B2BF7F1B100CAA076 /* ToastOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03500C2A2BF7F1B100CAA076 /* ToastOverlayView.swift */; };\n\t\t03500C2D2BF7FC2500CAA076 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03500C2C2BF7FC2500CAA076 /* ToastView.swift */; };\n\t\t03531EEC2C2D81DC004A3464 /* LinkSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03531EEB2C2D81DC004A3464 /* LinkSettingsView.swift */; };\n\t\t03531EEE2C2D9298004A3464 /* SearchSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03531EED2C2D9298004A3464 /* SearchSheetView.swift */; };\n\t\t03531EF12C2DA298004A3464 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03531EF02C2DA298004A3464 /* SearchResultsView.swift */; };\n\t\t03531EF52C2DA610004A3464 /* NavigationSearchType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03531EF42C2DA610004A3464 /* NavigationSearchType.swift */; };\n\t\t035394862C9CDC1100795AA5 /* SubscriptionListNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035394852C9CDC1100795AA5 /* SubscriptionListNavigationButton.swift */; };\n\t\t0353948B2CA076D000795AA5 /* InboxView+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0353948A2CA076D000795AA5 /* InboxView+Views.swift */; };\n\t\t0353948D2CA080EB00795AA5 /* FeedWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0353948C2CA080EB00795AA5 /* FeedWelcomeView.swift */; };\n\t\t0353948F2CA088E600795AA5 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0353948E2CA088E600795AA5 /* DeveloperSettingsView.swift */; };\n\t\t035394932CA1AE2C00795AA5 /* UptimeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035394922CA1AE2C00795AA5 /* UptimeData.swift */; };\n\t\t035394952CA1AE6300795AA5 /* InstanceUptimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035394942CA1AE6300795AA5 /* InstanceUptimeView.swift */; };\n\t\t035394992CA1B20B00795AA5 /* InstanceView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035394982CA1B20B00795AA5 /* InstanceView+Logic.swift */; };\n\t\t0353949C2CA4B3E800795AA5 /* CrossPostListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0353949B2CA4B3E800795AA5 /* CrossPostListView.swift */; };\n\t\t0355F9462C150B2300605248 /* ExternalApiInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0355F9452C150B2300605248 /* ExternalApiInfoView.swift */; };\n\t\t035BE0872BDD8DA000F77D73 /* NavigationRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BE0862BDD8DA000F77D73 /* NavigationRootView.swift */; };\n\t\t035BE0892BDD901B00F77D73 /* NavigationPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BE0882BDD901B00F77D73 /* NavigationPage.swift */; };\n\t\t035BE08B2BDD903100F77D73 /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BE08A2BDD903100F77D73 /* NavigationModel.swift */; };\n\t\t035BE08D2BDE88EC00F77D73 /* NavigationLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BE08C2BDE88EC00F77D73 /* NavigationLayerView.swift */; };\n\t\t035BE08F2BDE911900F77D73 /* NavigationLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BE08E2BDE911900F77D73 /* NavigationLayer.swift */; };\n\t\t035BE0912BDEA01E00F77D73 /* View+NavigationSheetModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BE0902BDEA01E00F77D73 /* View+NavigationSheetModifiers.swift */; };\n\t\t035DF9112EB2AA160021DE8C /* PersonContentGridView+FeedLoaderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DF9102EB2AA160021DE8C /* PersonContentGridView+FeedLoaderType.swift */; };\n\t\t035DF9132EB2AF8E0021DE8C /* GetContentFilter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DF9122EB2AF8E0021DE8C /* GetContentFilter+Extensions.swift */; };\n\t\t035DFA232EB3F7240021DE8C /* CommunityAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DFA222EB3F7240021DE8C /* CommunityAboutView.swift */; };\n\t\t035DFA252EB3FB550021DE8C /* CommunityDescriptionEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DFA242EB3FB550021DE8C /* CommunityDescriptionEditorView.swift */; };\n\t\t035EDEF12C2DE94B00F51144 /* DefaultTextInputType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035EDEED2C2DE94B00F51144 /* DefaultTextInputType.swift */; };\n\t\t035EDEF22C2DE94B00F51144 /* _assignIfNotEqual.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035EDEEC2C2DE94B00F51144 /* _assignIfNotEqual.swift */; };\n\t\t035EDEF32C2DE94B00F51144 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035EDEEE2C2DE94B00F51144 /* SearchBar.swift */; };\n\t\t035EDEF42C2DE94B00F51144 /* SearchBar+NavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035EDEEF2C2DE94B00F51144 /* SearchBar+NavigationView.swift */; };\n\t\t035EDEF52C2DE94B00F51144 /* SearchBarExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035EDEF02C2DE94B00F51144 /* SearchBarExtensions.swift */; };\n\t\t035EDEFB2C2DF98700F51144 /* CommunityListRowBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035EDEFA2C2DF98700F51144 /* CommunityListRowBody.swift */; };\n\t\t035EDF012C2ECFE000F51144 /* Searchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035EDF002C2ECFE000F51144 /* Searchable.swift */; };\n\t\t035EDF032C2ED0DE00F51144 /* PersonListRowBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035EDF022C2ED0DE00F51144 /* PersonListRowBody.swift */; };\n\t\t03600D932D4531DF00C704CB /* PrivacyBypassImageProxySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03600D922D4531DF00C704CB /* PrivacyBypassImageProxySettingsView.swift */; };\n\t\t0368799A2DA1320000E796EF /* ComponentViews in Frameworks */ = {isa = PBXBuildFile; productRef = 036879992DA1320000E796EF /* ComponentViews */; };\n\t\t0368F3432D72796B007DEB70 /* LanguagePickerSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0368F3422D72796B007DEB70 /* LanguagePickerSheetView.swift */; };\n\t\t0368F34D2D733215007DEB70 /* SortTimeRange+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0368F34C2D733215007DEB70 /* SortTimeRange+Extensions.swift */; };\n\t\t0368F34F2D734066007DEB70 /* SearchSortType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0368F34E2D734066007DEB70 /* SearchSortType+Extensions.swift */; };\n\t\t0368F3692D7349D8007DEB70 /* LanguageListRowBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0368F3682D7349D8007DEB70 /* LanguageListRowBody.swift */; };\n\t\t0369B3532BFA514B001EFEDF /* ToastLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0369B3522BFA514B001EFEDF /* ToastLocation.swift */; };\n\t\t0369B3562BFA6824001EFEDF /* InboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0369B3552BFA6824001EFEDF /* InboxView.swift */; };\n\t\t0369B35D2BFB86E3001EFEDF /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0369B35A2BFB86E3001EFEDF /* Account.swift */; };\n\t\t036A84552D98253400E95D50 /* UpdateBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A84542D98253400E95D50 /* UpdateBannerView.swift */; };\n\t\t036A84D82D99531400E95D50 /* View+ConditionalNavigationTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A84D72D99531400E95D50 /* View+ConditionalNavigationTitle.swift */; };\n\t\t036CC3AF2B8145C30098B6A1 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036CC3AE2B8145C30098B6A1 /* AppState.swift */; };\n\t\t036ED6792D0AF3740018E5EA /* PinnedSortTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED6782D0AF3740018E5EA /* PinnedSortTracker.swift */; };\n\t\t036ED67B2D0B004D0018E5EA /* AdvancedSortView+SortButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED67A2D0B004D0018E5EA /* AdvancedSortView+SortButton.swift */; };\n\t\t036ED67D2D0B006C0018E5EA /* TopSortPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED67C2D0B006C0018E5EA /* TopSortPicker.swift */; };\n\t\t036ED67F2D0B9A520018E5EA /* CommunitySearchSortPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED67E2D0B9A520018E5EA /* CommunitySearchSortPicker.swift */; };\n\t\t036ED6832D0C483B0018E5EA /* ProfileProviding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED6822D0C483B0018E5EA /* ProfileProviding+Extensions.swift */; };\n\t\t036FFA2D2D45110C00998D8A /* ChangePasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036FFA2C2D45110C00998D8A /* ChangePasswordView.swift */; };\n\t\t036FFA2F2D45197300998D8A /* PrivacySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036FFA2E2D45197300998D8A /* PrivacySettingsView.swift */; };\n\t\t0370299D2D6B70F400B749DF /* MockApiClient+Realistic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370299C2D6B70F400B749DF /* MockApiClient+Realistic.swift */; };\n\t\t0370299F2D6B743B00B749DF /* PostMockType+Realistic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370299E2D6B743B00B749DF /* PostMockType+Realistic.swift */; };\n\t\t037029A12D6B9A3900B749DF /* View+TabBarPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037029A02D6B9A3900B749DF /* View+TabBarPreview.swift */; };\n\t\t037029A32D6B9B8400B749DF /* ContentView+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037029A22D6B9B8400B749DF /* ContentView+Tab.swift */; };\n\t\t0372EBCE2D36FBCF00257095 /* RegistrationApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0372EBCD2D36FBCF00257095 /* RegistrationApplication+Extensions.swift */; };\n\t\t0372EC202D370F0200257095 /* RegistrationApplicationDenialEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0372EC1F2D370F0200257095 /* RegistrationApplicationDenialEditorView.swift */; };\n\t\t037331A42C9CB12D00C826E1 /* EnvironmentValues+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037331A32C9CB12D00C826E1 /* EnvironmentValues+Extensions.swift */; };\n\t\t037352332F27A83900341673 /* PostPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037352322F27A83900341673 /* PostPollView.swift */; };\n\t\t037386472BDAFE81007492B5 /* LemmyMarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 037386462BDAFE81007492B5 /* LemmyMarkdownUI */; };\n\t\t0377BD752DE219A400E38593 /* OnboardingUsernameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0377BD742DE219A400E38593 /* OnboardingUsernameView.swift */; };\n\t\t0377BD792DE22D4E00E38593 /* UsernameValidity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0377BD782DE22D4E00E38593 /* UsernameValidity+Extensions.swift */; };\n\t\t0377BE072DE645E100E38593 /* OnboardingRecommendInstanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0377BE062DE645E100E38593 /* OnboardingRecommendInstanceView.swift */; };\n\t\t0377BE292DE7A2DE00E38593 /* Haptics in Frameworks */ = {isa = PBXBuildFile; productRef = 0377BE282DE7A2DE00E38593 /* Haptics */; };\n\t\t0377BE3B2DE8E70D00E38593 /* HapticLevel+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0377BE3A2DE8E70D00E38593 /* HapticLevel+Extensions.swift */; };\n\t\t0377BE9B2DEA328900E38593 /* OnboardingEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0377BE9A2DEA328900E38593 /* OnboardingEmailView.swift */; };\n\t\t0377BE9F2DEA361600E38593 /* OnboardingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0377BE9E2DEA361600E38593 /* OnboardingModel.swift */; };\n\t\t0377BF9B2DF0E0E000E38593 /* Rest in Frameworks */ = {isa = PBXBuildFile; productRef = 0377BF9A2DF0E0E000E38593 /* Rest */; };\n\t\t037DE0752CE023E3007F7B92 /* BlockListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037DE0742CE023E3007F7B92 /* BlockListView.swift */; };\n\t\t037DE07A2CE108D9007F7B92 /* FooterLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037DE0792CE108D9007F7B92 /* FooterLinkView.swift */; };\n\t\t037F77ED2D3B064B00D4E180 /* SettingsDeviceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F77EC2D3B064B00D4E180 /* SettingsDeviceView.swift */; };\n\t\t037F783F2D3BD05500D4E180 /* PostSettingsView+PostSizePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F783E2D3BD05500D4E180 /* PostSettingsView+PostSizePicker.swift */; };\n\t\t037F78412D3C00E600D4E180 /* SettingsInteractionBarSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F78402D3C00E600D4E180 /* SettingsInteractionBarSummaryView.swift */; };\n\t\t037F78432D3C129B00D4E180 /* PostThumbnailSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F78422D3C129B00D4E180 /* PostThumbnailSettingsView.swift */; };\n\t\t037F78452D3C25AB00D4E180 /* PostSubscriptionIndicatorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F78442D3C25AB00D4E180 /* PostSubscriptionIndicatorSettingsView.swift */; };\n\t\t037FC0702E4A6B16009E3E63 /* InstanceView+About.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037FC06F2E4A6B16009E3E63 /* InstanceView+About.swift */; };\n\t\t038028D32CAB3D2D0091A8A2 /* ShareActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038028D22CAB3D2D0091A8A2 /* ShareActivity.swift */; };\n\t\t038028D52CAB479D0091A8A2 /* PostEditorView+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038028D42CAB479D0091A8A2 /* PostEditorView+Views.swift */; };\n\t\t038028D82CACAB960091A8A2 /* ModeratorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038028D72CACAB960091A8A2 /* ModeratorSettingsView.swift */; };\n\t\t038028DA2CACACD30091A8A2 /* PostEllipsisMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038028D92CACACD30091A8A2 /* PostEllipsisMenus.swift */; };\n\t\t038028F62CB096960091A8A2 /* SearchView+FilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038028F52CB096960091A8A2 /* SearchView+FilterModels.swift */; };\n\t\t038028F82CB097A10091A8A2 /* SearchView+InstancePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038028F72CB097A10091A8A2 /* SearchView+InstancePicker.swift */; };\n\t\t038028FA2CB097CB0091A8A2 /* SearchView+LocationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038028F92CB097CB0091A8A2 /* SearchView+LocationPicker.swift */; };\n\t\t038028FD2CB72A2A0091A8A2 /* ContentRemovalEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038028FC2CB72A2A0091A8A2 /* ContentRemovalEditorView.swift */; };\n\t\t038028FF2CB72AC90091A8A2 /* ReasonShortcutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038028FE2CB72AC90091A8A2 /* ReasonShortcutView.swift */; };\n\t\t0380965F2C10AA80003ED1D8 /* AppState+Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0380965E2C10AA80003ED1D8 /* AppState+Transition.swift */; };\n\t\t038096612C10AAD8003ED1D8 /* TransitionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038096602C10AAD8003ED1D8 /* TransitionView.swift */; };\n\t\t038100512F6AE867008A7731 /* MlemBackend in Frameworks */ = {isa = PBXBuildFile; productRef = 038100502F6AE867008A7731 /* MlemBackend */; };\n\t\t038188992D43E0F30073E88D /* SafetySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038188982D43E0F30073E88D /* SafetySettingsView.swift */; };\n\t\t0381889B2D4412A40073E88D /* SafetyBlurNsfwSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381889A2D4412A40073E88D /* SafetyBlurNsfwSettingsView.swift */; };\n\t\t0381F7142F670F95008A7731 /* SwipeActionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381F7132F670F95008A7731 /* SwipeActionConfiguration.swift */; };\n\t\t0381F7162F671427008A7731 /* PostBarConfiguration+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381F7152F671427008A7731 /* PostBarConfiguration+Types.swift */; };\n\t\t0381F7242F672258008A7731 /* CommentBarConfiguration+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381F7232F672258008A7731 /* CommentBarConfiguration+Types.swift */; };\n\t\t0381F7282F6724D3008A7731 /* ReplyBarConfiguration+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381F7272F6724D3008A7731 /* ReplyBarConfiguration+Types.swift */; };\n\t\t0382A7F02C09F0F800C79DDA /* PersonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0382A7EF2C09F0F800C79DDA /* PersonView.swift */; };\n\t\t0382A7F22C0A758E00C79DDA /* ProfileDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0382A7F12C0A758E00C79DDA /* ProfileDateView.swift */; };\n\t\t0382A7F42C0A76A900C79DDA /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0382A7F32C0A76A900C79DDA /* Date+Extensions.swift */; };\n\t\t0389DDC32C38907C0005B808 /* Message1Providing+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDC22C38907C0005B808 /* Message1Providing+Extensions.swift */; };\n\t\t0389DDC52C38917A0005B808 /* InboxItemProviding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDC42C38917A0005B808 /* InboxItemProviding+Extensions.swift */; };\n\t\t0389DDC72C389F840005B808 /* UnreadCount+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDC62C389F840005B808 /* UnreadCount+Extensions.swift */; };\n\t\t0389DDC92C39658E0005B808 /* Binding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDC82C39658E0005B808 /* Binding+Extensions.swift */; };\n\t\t0389DDCF2C39CB0E0005B808 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDCE2C39CB0E0005B808 /* SearchView.swift */; };\n\t\t0389DDD12C39E1030005B808 /* InstanceListRowBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDD02C39E1030005B808 /* InstanceListRowBody.swift */; };\n\t\t0389DDD32C39E4D40005B808 /* PasteLinkButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDD22C39E4D40005B808 /* PasteLinkButtonView.swift */; };\n\t\t0389DDD52C39F1290005B808 /* CommunityListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDD42C39F1290005B808 /* CommunityListRow.swift */; };\n\t\t0389DDDB2C3AB6340005B808 /* ActionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDDA2C3AB6340005B808 /* ActionBuilder.swift */; };\n\t\t038C85692D861A2100543F70 /* Comment+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C85682D861A2100543F70 /* Comment+Mock.swift */; };\n\t\t038C85D82D87696F00543F70 /* FeedToolbarOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C85D72D87696F00543F70 /* FeedToolbarOptions.swift */; };\n\t\t038C85E62D88337100543F70 /* CommentMockType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C85E52D88337100543F70 /* CommentMockType.swift */; };\n\t\t038C86572D888EC100543F70 /* CommentSortType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C86562D888EC100543F70 /* CommentSortType+Extensions.swift */; };\n\t\t038E1ABC2F58B9EF00D30F01 /* CommunityActionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038E1ABB2F58B9EF00D30F01 /* CommunityActionConfiguration.swift */; };\n\t\t038E1AC02F58C76A00D30F01 /* SwipeActionEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038E1ABF2F58C76A00D30F01 /* SwipeActionEditorView.swift */; };\n\t\t038E1ACA2F59C18100D30F01 /* CommunitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038E1AC92F59C18100D30F01 /* CommunitySettingsView.swift */; };\n\t\t038E5C132F6D617C00C54DEB /* ImageViewerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038E5C122F6D617C00C54DEB /* ImageViewerSettingsView.swift */; };\n\t\t038E5E892F6D694500C54DEB /* ImageViewerShowControlsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038E5E882F6D694500C54DEB /* ImageViewerShowControlsSettingsView.swift */; };\n\t\t038E5E8B2F6D6C3800C54DEB /* ImageViewerDismissSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038E5E8A2F6D6C3800C54DEB /* ImageViewerDismissSettingsView.swift */; };\n\t\t038E62E02F6F0FC600C54DEB /* ContextMenuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038E62DF2F6F0FC600C54DEB /* ContextMenuConfiguration.swift */; };\n\t\t0391E0F92CFF17AE0040CCA8 /* ProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0391E0F82CFF17AE0040CCA8 /* ProfileSettingsView.swift */; };\n\t\t0391E0FB2D0066240040CCA8 /* ImageUploadMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0391E0FA2D0066240040CCA8 /* ImageUploadMenu.swift */; };\n\t\t0391E0FE2D05B2DF0040CCA8 /* AdvancedSortView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0391E0FD2D05B2DF0040CCA8 /* AdvancedSortView.swift */; };\n\t\t0395BCF82D9C57DE00865B33 /* View+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0395BCF72D9C57DE00865B33 /* View+Background.swift */; };\n\t\t0397D4602C66113F002C6CDC /* CommentBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D45F2C66113F002C6CDC /* CommentBodyView.swift */; };\n\t\t0397D4642C676CA8002C6CDC /* FeedSortPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4632C676CA8002C6CDC /* FeedSortPicker.swift */; };\n\t\t0397D46C2C67E583002C6CDC /* SortingSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D46B2C67E583002C6CDC /* SortingSettingsView.swift */; };\n\t\t0397D4722C68078F002C6CDC /* PrefetchingConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4712C68078F002C6CDC /* PrefetchingConfiguration+Extensions.swift */; };\n\t\t0397D47A2C693444002C6CDC /* ReportEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4792C693444002C6CDC /* ReportEditorView.swift */; };\n\t\t0397D4802C693A88002C6CDC /* [BlockNode]+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D47F2C693A88002C6CDC /* [BlockNode]+Extensions.swift */; };\n\t\t0397D4862C6A24D2002C6CDC /* ReportableProviding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4852C6A24D2002C6CDC /* ReportableProviding+Extensions.swift */; };\n\t\t0397D48C2C6BE9A2002C6CDC /* CollapsibleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D48B2C6BE9A2002C6CDC /* CollapsibleSection.swift */; };\n\t\t0397D4912C6CE871002C6CDC /* PostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D48F2C6CE871002C6CDC /* PostEditorView.swift */; };\n\t\t0397D4932C6CE87E002C6CDC /* PostEditorTargetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4922C6CE87E002C6CDC /* PostEditorTargetView.swift */; };\n\t\t0397D49A2C6EA6EE002C6CDC /* InteractionBarEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4992C6EA6EE002C6CDC /* InteractionBarEditorView.swift */; };\n\t\t0397D49C2C6EA73C002C6CDC /* InteractionBarConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D49B2C6EA73C002C6CDC /* InteractionBarConfiguration.swift */; };\n\t\t0397D4A22C6EB035002C6CDC /* ActionAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4A12C6EB035002C6CDC /* ActionAppearance.swift */; };\n\t\t0397D4A42C6FBC04002C6CDC /* ActionAppearance+StaticValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4A32C6FBC04002C6CDC /* ActionAppearance+StaticValues.swift */; };\n\t\t039D75642C4EEE69004F24C2 /* DeletableProviding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039D75632C4EEE69004F24C2 /* DeletableProviding+Extensions.swift */; };\n\t\t039EFEC32BEEBEE0003AC372 /* LoginInstancePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039EFEC22BEEBEE0003AC372 /* LoginInstancePickerView.swift */; };\n\t\t039F58812C7A7E5900C61658 /* JumpButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F58802C7A7E5900C61658 /* JumpButtonView.swift */; };\n\t\t039F58822C7A7EF300C61658 /* ToolbarEllipsisMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0E07002C12707700445849 /* ToolbarEllipsisMenu.swift */; };\n\t\t039F58842C7A7F2C00C61658 /* CommentJumpButtonLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F58832C7A7F2C00C61658 /* CommentJumpButtonLocation.swift */; };\n\t\t039F58862C7A810100C61658 /* ExpandedPostView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F58852C7A810100C61658 /* ExpandedPostView+Logic.swift */; };\n\t\t039F58882C7B531800C61658 /* SquircleLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F58872C7B531800C61658 /* SquircleLabelStyle.swift */; };\n\t\t039F588A2C7B54FE00C61658 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F58892C7B54FE00C61658 /* GeneralSettingsView.swift */; };\n\t\t039F588C2C7B574E00C61658 /* AdvancedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F588B2C7B574E00C61658 /* AdvancedSettingsView.swift */; };\n\t\t039F588F2C7B599800C61658 /* ThemeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F588E2C7B599800C61658 /* ThemeLabel.swift */; };\n\t\t039F58912C7B5C7A00C61658 /* ContentView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F58902C7B5C7A00C61658 /* ContentView+Logic.swift */; };\n\t\t039F58932C7B616600C61658 /* CommentSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F58922C7B616600C61658 /* CommentSettingsView.swift */; };\n\t\t039F58952C7B618F00C61658 /* InboxSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F58942C7B618F00C61658 /* InboxSettingsView.swift */; };\n\t\t039F58972C7B68F100C61658 /* AboutMlemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F58962C7B68F100C61658 /* AboutMlemView.swift */; };\n\t\t039F58992C7B697D00C61658 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F58982C7B697D00C61658 /* Bundle+Extensions.swift */; };\n\t\t03A630ED2D497005009A47A6 /* ExternalLinkSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A630EC2D497005009A47A6 /* ExternalLinkSettingsView.swift */; };\n\t\t03A630EF2D497143009A47A6 /* TappableLinksSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A630EE2D497143009A47A6 /* TappableLinksSettingsView.swift */; };\n\t\t03A630F12D497674009A47A6 /* ShieldsBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A630F02D497674009A47A6 /* ShieldsBadgeView.swift */; };\n\t\t03A630F42D4976EB009A47A6 /* ShieldsBadgeView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A630F32D4976EB009A47A6 /* ShieldsBadgeView+Logic.swift */; };\n\t\t03A6315E2D4D1A1B009A47A6 /* DefaultFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A6315D2D4D1A1B009A47A6 /* DefaultFeedSettingsView.swift */; };\n\t\t03A631602D4D1CBB009A47A6 /* HapticSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A6315F2D4D1CBB009A47A6 /* HapticSettingsView.swift */; };\n\t\t03A6316D2D4E3D24009A47A6 /* ModeratorActionSeparationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A6316C2D4E3D24009A47A6 /* ModeratorActionSeparationSettingsView.swift */; };\n\t\t03A631CC2D4FD18C009A47A6 /* Post+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A631CA2D4FCEF7009A47A6 /* Post+Mock.swift */; };\n\t\t03A814292ED1BCA90023E9E8 /* ModlogView+Filters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A814282ED1BCA90023E9E8 /* ModlogView+Filters.swift */; };\n\t\t03A818AD2EDCDBA20023E9E8 /* FederationMode+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A818AC2EDCDBA20023E9E8 /* FederationMode+Extensions.swift */; };\n\t\t03A82FA12C0D1E8500D01A5C /* ApiClient+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A82FA02C0D1E8500D01A5C /* ApiClient+Extensions.swift */; };\n\t\t03A82FA32C0D1F2400D01A5C /* View+ExternalApiWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A82FA22C0D1F2400D01A5C /* View+ExternalApiWarning.swift */; };\n\t\t03A9FD162D7CEC09007A734D /* Theming in Frameworks */ = {isa = PBXBuildFile; productRef = 03A9FD152D7CEC09007A734D /* Theming */; };\n\t\t03A9FD182D7CFC20007A734D /* Palette+Oled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A9FD172D7CFC20007A734D /* Palette+Oled.swift */; };\n\t\t03A9FD1A2D7CFC69007A734D /* Palette+Monochrome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A9FD192D7CFC69007A734D /* Palette+Monochrome.swift */; };\n\t\t03A9FD1C2D7CFD5C007A734D /* Palette+Solarized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A9FD1B2D7CFD5C007A734D /* Palette+Solarized.swift */; };\n\t\t03A9FD1E2D7CFF0D007A734D /* Palette+Dracula.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A9FD1D2D7CFF0D007A734D /* Palette+Dracula.swift */; };\n\t\t03A9FD202D7D0072007A734D /* PaletteOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A9FD1F2D7D0072007A734D /* PaletteOption.swift */; };\n\t\t03AB484F2CBAE33500567FF9 /* MarkdownWithLinkList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AB484E2CBAE33500567FF9 /* MarkdownWithLinkList.swift */; };\n\t\t03AB48522CBC042E00567FF9 /* AccountContentSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AB48512CBC042E00567FF9 /* AccountContentSettingsView.swift */; };\n\t\t03AB48552CBC0B8000567FF9 /* AccountAdvancedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AB48542CBC0B8000567FF9 /* AccountAdvancedSettingsView.swift */; };\n\t\t03AB48572CBC0DFC00567FF9 /* AccountSignInSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AB48562CBC0DFC00567FF9 /* AccountSignInSettingsView.swift */; };\n\t\t03AB48592CBC14CE00567FF9 /* AccountEmailSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AB48582CBC14CE00567FF9 /* AccountEmailSettingsView.swift */; };\n\t\t03AB906F2D418C200054D9E1 /* CommentJumpButtonSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AB906E2D418C200054D9E1 /* CommentJumpButtonSettingsView.swift */; };\n\t\t03ABE5B62DB79A0E00374AFF /* DateComponents+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ABE5B52DB79A0E00374AFF /* DateComponents+Extensions.swift */; };\n\t\t03ABE5E42DB8E57000374AFF /* AccountAgeVisibilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ABE5E32DB8E57000374AFF /* AccountAgeVisibilitySettingsView.swift */; };\n\t\t03ACE71A2DFD57CC00FD66C0 /* SiteSoftware+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACE7192DFD57CC00FD66C0 /* SiteSoftware+Extensions.swift */; };\n\t\t03AD09E82CF88007001EF9F7 /* MoreRepliesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AD09E72CF88007001EF9F7 /* MoreRepliesButton.swift */; };\n\t\t03AD0A822CFDBFA0001EF9F7 /* AccountLocalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AD0A812CFDBFA0001EF9F7 /* AccountLocalSettingsView.swift */; };\n\t\t03AD0A842CFDC557001EF9F7 /* AccountNicknameFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AD0A832CFDC557001EF9F7 /* AccountNicknameFieldView.swift */; };\n\t\t03AF91DD2C1B23E500E56644 /* ImageViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AF91DC2C1B23E500E56644 /* ImageViewer.swift */; };\n\t\t03AF91E12C1B25DE00E56644 /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AF91E02C1B25DE00E56644 /* UIDevice+Extensions.swift */; };\n\t\t03AF91E32C1C616F00E56644 /* InteractionBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AF91E22C1C616F00E56644 /* InteractionBarView.swift */; };\n\t\t03AF91E52C1C61FA00E56644 /* PostBarConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AF91E42C1C61FA00E56644 /* PostBarConfiguration.swift */; };\n\t\t03AF91EA2C1CE96600E56644 /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AF91E92C1CE96600E56644 /* Counter.swift */; };\n\t\t03AFD0DF2C3B2E000054B8AD /* PersonListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AFD0DE2C3B2E000054B8AD /* PersonListRow.swift */; };\n\t\t03AFD0E12C3B30390054B8AD /* InstanceListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AFD0E02C3B30390054B8AD /* InstanceListRow.swift */; };\n\t\t03AFD0E32C3C0C540054B8AD /* InstanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AFD0E22C3C0C540054B8AD /* InstanceView.swift */; };\n\t\t03B045F62E26D64900540EFB /* SiteSoftwareType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B045F52E26D64900540EFB /* SiteSoftwareType+Extensions.swift */; };\n\t\t03B04FC02C5FC32300824128 /* SimpleAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B04FBF2C5FC32300824128 /* SimpleAvatarView.swift */; };\n\t\t03B0EB6F2C87827A00F79FDF /* ExpandedPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B0EB6E2C87827A00F79FDF /* ExpandedPostView.swift */; };\n\t\t03B25B2F2CC43F8600EB6DF5 /* InstanceSafetyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B25B2E2CC43F8600EB6DF5 /* InstanceSafetyView.swift */; };\n\t\t03B25B312CC4403500EB6DF5 /* Fediseer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B25B302CC4403500EB6DF5 /* Fediseer.swift */; };\n\t\t03B25B332CC440A600EB6DF5 /* FediseerOpinionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B25B322CC440A600EB6DF5 /* FediseerOpinionView.swift */; };\n\t\t03B25B352CC4446400EB6DF5 /* FediseerOpinionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B25B342CC4446400EB6DF5 /* FediseerOpinionListView.swift */; };\n\t\t03B25B372CC4478600EB6DF5 /* FediseerInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B25B362CC4478600EB6DF5 /* FediseerInfoView.swift */; };\n\t\t03B25B3B2CC44FFF00EB6DF5 /* UploadConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B25B3A2CC44FFF00EB6DF5 /* UploadConfirmationView.swift */; };\n\t\t03B431B42C4481C3001A1EB5 /* MarkdownTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B431B32C4481C3001A1EB5 /* MarkdownTextEditor.swift */; };\n\t\t03B431B62C454D49001A1EB5 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B431B52C454D49001A1EB5 /* UIImage+Extensions.swift */; };\n\t\t03B431BC2C455838001A1EB5 /* LargePostBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B431BB2C455838001A1EB5 /* LargePostBodyView.swift */; };\n\t\t03B431C02C45ABFB001A1EB5 /* UITextView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B431BF2C45ABFB001A1EB5 /* UITextView+Extensions.swift */; };\n\t\t03B431C22C45BA00001A1EB5 /* MarkdownEditorToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B431C12C45BA00001A1EB5 /* MarkdownEditorToolbarView.swift */; };\n\t\t03B431C42C45BA45001A1EB5 /* AccountPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B431C32C45BA45001A1EB5 /* AccountPickerMenu.swift */; };\n\t\t03B62B772CE295530077E9C8 /* RulesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B62B762CE295530077E9C8 /* RulesListView.swift */; };\n\t\t03B62B792CE2A2C00077E9C8 /* RulesPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B62B782CE2A2C00077E9C8 /* RulesPickerView.swift */; };\n\t\t03B62C402CE811CB0077E9C8 /* PersonBanEditorView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B62C3F2CE811CB0077E9C8 /* PersonBanEditorView+Logic.swift */; };\n\t\t03B72B672C2888EE0023A6C4 /* View+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B72B662C2888EE0023A6C4 /* View+ContextMenu.swift */; };\n\t\t03B72B6B2C28A0190023A6C4 /* SubscriptionListSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B72B6A2C28A0190023A6C4 /* SubscriptionListSettingsView.swift */; };\n\t\t03B7F3352EEEC70F00B00F6A /* NoteEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B7F3342EEEC70F00B00F6A /* NoteEditorView.swift */; };\n\t\t03BF11C32D3D135D00CC1F66 /* SearchView+CreatorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF11C22D3D135D00CC1F66 /* SearchView+CreatorPicker.swift */; };\n\t\t03BF11C52D3D3AFB00CC1F66 /* PostReadIndicatorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF11C42D3D3AFB00CC1F66 /* PostReadIndicatorSettingsView.swift */; };\n\t\t03BF11C72D3D634A00CC1F66 /* AccessibilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF11C62D3D634A00CC1F66 /* AccessibilitySettingsView.swift */; };\n\t\t03BF11CA2D4027E900CC1F66 /* DevicePickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF11C92D4027E900CC1F66 /* DevicePickerItem.swift */; };\n\t\t03BF11CC2D404F2C00CC1F66 /* CommentMaximumDepthSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF11CB2D404F2C00CC1F66 /* CommentMaximumDepthSettingsView.swift */; };\n\t\t03C93CF02BEFFB1A00327BFE /* LoginCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C93CEF2BEFFB1A00327BFE /* LoginCredentialsView.swift */; };\n\t\t03CBD18D2C6120F600E870BC /* PersonFlair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CBD18C2C6120F600E870BC /* PersonFlair.swift */; };\n\t\t03CCDAA02BF2795300C0C851 /* LoginPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CCDA9F2BF2795300C0C851 /* LoginPage.swift */; };\n\t\t03CCDAA42BF2852E00C0C851 /* LoginTotpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CCDAA32BF2852E00C0C851 /* LoginTotpView.swift */; };\n\t\t03D001DD2EC53A97001BF97D /* NavigationPage+PresentationDetents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D001DC2EC53A97001BF97D /* NavigationPage+PresentationDetents.swift */; };\n\t\t03D006322ECCBA95001BF97D /* QuickSwipeAction+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D006312ECCBA95001BF97D /* QuickSwipeAction+Actions.swift */; };\n\t\t03D0273C2CD3BA5100984519 /* PersonContent+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D0273B2CD3BA5100984519 /* PersonContent+Extensions.swift */; };\n\t\t03D283FA2D256E1E00A6659B /* VisitHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D283F92D256E1E00A6659B /* VisitHistory.swift */; };\n\t\t03D283FC2D25A3F700A6659B /* VisitHistory+CodedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D283FB2D25A3F700A6659B /* VisitHistory+CodedData.swift */; };\n\t\t03D283FE2D25EEC500A6659B /* SearchView+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D283FD2D25EEC500A6659B /* SearchView+Views.swift */; };\n\t\t03D284002D26F09500A6659B /* Instance+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D283FF2D26F09500A6659B /* Instance+Extensions.swift */; };\n\t\t03D284022D29E03C00A6659B /* FeedFilterButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D284012D29E03C00A6659B /* FeedFilterButtonStyle.swift */; };\n\t\t03D284062D2AEE3A00A6659B /* TabBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D284052D2AEE3A00A6659B /* TabBarSettingsView.swift */; };\n\t\t03D2A6372C00F92400ED4FF2 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D2A6362C00F92400ED4FF2 /* Session.swift */; };\n\t\t03D2A6392C00FAE000ED4FF2 /* UserAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D2A6382C00FAE000ED4FF2 /* UserAccount.swift */; };\n\t\t03D2A63B2C010B7500ED4FF2 /* GuestAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D2A63A2C010B7500ED4FF2 /* GuestAccount.swift */; };\n\t\t03D2A63D2C010CD400ED4FF2 /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D2A63C2C010CD400ED4FF2 /* UserSession.swift */; };\n\t\t03D2A63F2C010DBF00ED4FF2 /* GuestSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D2A63E2C010DBF00ED4FF2 /* GuestSession.swift */; };\n\t\t03D2A6422C011F4A00ED4FF2 /* AccountListRowBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D2A6412C011F4A00ED4FF2 /* AccountListRowBody.swift */; };\n\t\t03D3A1D42BB88EF1009DE55E /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A1D32BB88EF1009DE55E /* Action.swift */; };\n\t\t03D3A1E52BB8B7A3009DE55E /* ActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A1E42BB8B7A3009DE55E /* ActionType.swift */; };\n\t\t03D3A1EF2BB9CA1D009DE55E /* MenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A1EE2BB9CA1D009DE55E /* MenuButton.swift */; };\n\t\t03D3A1F12BB9D48E009DE55E /* BasicAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A1F02BB9D48E009DE55E /* BasicAction.swift */; };\n\t\t03D3A1F32BB9D49B009DE55E /* ActionGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A1F22BB9D49B009DE55E /* ActionGroup.swift */; };\n\t\t03D65D702F4B046F0041ADAF /* ContextMenuSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D65D6F2F4B046F0041ADAF /* ContextMenuSettingsView.swift */; };\n\t\t03D662A52F5377630041ADAF /* ActionSeedSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D662A42F5377630041ADAF /* ActionSeedSections.swift */; };\n\t\t03D8BF432DA55B6900506687 /* Icons in Frameworks */ = {isa = PBXBuildFile; productRef = 03D8BF422DA55B6900506687 /* Icons */; };\n\t\t03DA4FB72CF115FB001C3C77 /* CommentEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B431B12C44409D001A1EB5 /* CommentEditorView.swift */; };\n\t\t03DAEA772C64074E0064DE64 /* SubscriptionListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DAEA762C64074E0064DE64 /* SubscriptionListItemView.swift */; };\n\t\t03DD69422D4FDE8900F8950D /* Person+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DD69412D4FDE8900F8950D /* Person+Mock.swift */; };\n\t\t03DD69442D4FEA0000F8950D /* PreviewModifier+SampleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DD69432D4FEA0000F8950D /* PreviewModifier+SampleEnvironment.swift */; };\n\t\t03E0EF432CA73D7A002CB66C /* PostStubResolutionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E0EF422CA73D7A002CB66C /* PostStubResolutionPage.swift */; };\n\t\t03E0EF452CA74036002CB66C /* CommentPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E0EF442CA74036002CB66C /* CommentPage.swift */; };\n\t\t03E46ACB2D1216CE002589DB /* PostViewLinkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E46ACA2D1216CE002589DB /* PostViewLinkType.swift */; };\n\t\t03E46ACD2D121C19002589DB /* HeadlinePostBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E46ACC2D121C19002589DB /* HeadlinePostBodyView.swift */; };\n\t\t03E46AD22D130681002589DB /* VotesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E46AD12D130681002589DB /* VotesListView.swift */; };\n\t\t03E46AD42D130728002589DB /* ScoringOperation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E46AD32D130728002589DB /* ScoringOperation+Extensions.swift */; };\n\t\t03E614E52C0BCCAA00F692A4 /* PostSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB2EC7A2BFADA8B00DBC0EF /* PostSize.swift */; };\n\t\t03E614E72C0BCDC200F692A4 /* FullyQualifiedLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E614E62C0BCDC200F692A4 /* FullyQualifiedLinkView.swift */; };\n\t\t03EC83252E916C51004698BB /* View+SafeAreaBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC83242E916C51004698BB /* View+SafeAreaBar.swift */; };\n\t\t03EC83EE2E958A44004698BB /* OpenGraph in Frameworks */ = {isa = PBXBuildFile; productRef = 03EC83ED2E958A44004698BB /* OpenGraph */; };\n\t\t03EC83F02E9590D3004698BB /* LinkHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC83EF2E9590D3004698BB /* LinkHostView.swift */; };\n\t\t03EC84422E959AA5004698BB /* PostEditorWebsitePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC84412E959AA5004698BB /* PostEditorWebsitePreviewView.swift */; };\n\t\t03EC85EC2E9D8F37004698BB /* Actions in Frameworks */ = {isa = PBXBuildFile; productRef = 03EC85EB2E9D8F37004698BB /* Actions */; };\n\t\t03EC86462E9E9D4D004698BB /* PopupAnchorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC86452E9E9D4C004698BB /* PopupAnchorModel.swift */; };\n\t\t03ECD7192C81195000D48BF6 /* PostEditorView+LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ECD7182C81195000D48BF6 /* PostEditorView+LinkView.swift */; };\n\t\t03ECD71B2C811D6700D48BF6 /* PostEditorView+ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ECD71A2C811D6700D48BF6 /* PostEditorView+ImageView.swift */; };\n\t\t03ECD71F2C864DB700D48BF6 /* ImageUploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ECD71E2C864DB700D48BF6 /* ImageUploadManager.swift */; };\n\t\t03ECD7212C8654BA00D48BF6 /* PostEditorView+Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ECD7202C8654BA00D48BF6 /* PostEditorView+Toolbar.swift */; };\n\t\t03F6BD942D500DED006A425E /* PersonMockType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F6BD932D500DED006A425E /* PersonMockType.swift */; };\n\t\t03F6BD982D500E2A006A425E /* PersonMockType+Realistic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F6BD972D500E2A006A425E /* PersonMockType+Realistic.swift */; };\n\t\t03F6BD9A2D501041006A425E /* SeededRandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F6BD992D501041006A425E /* SeededRandomNumberGenerator.swift */; };\n\t\t03F6BD9C2D501478006A425E /* ActorIdentifier+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F6BD9B2D501478006A425E /* ActorIdentifier+Mock.swift */; };\n\t\t03F6BDAD2D516615006A425E /* Community+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F6BDAC2D516615006A425E /* Community+Mock.swift */; };\n\t\t03F6BDAF2D516636006A425E /* CommunityMockType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F6BDAE2D516636006A425E /* CommunityMockType.swift */; };\n\t\t03F6BDB12D52AA00006A425E /* CommunityMockType+Realistic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F6BDB02D52AA00006A425E /* CommunityMockType+Realistic.swift */; };\n\t\t03F6BDBC2D52B7FE006A425E /* PostMockType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F6BDBB2D52B7FE006A425E /* PostMockType.swift */; };\n\t\t03F6BDF82D555F6E006A425E /* ModMailInteractionBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F6BDF72D555F6E006A425E /* ModMailInteractionBarSettingsView.swift */; };\n\t\t03F967272CE218110081C9A3 /* PersonBanEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F967262CE218110081C9A3 /* PersonBanEditorView.swift */; };\n\t\t03F9672B2CE221220081C9A3 /* Label+Profile1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F9672A2CE221220081C9A3 /* Label+Profile1.swift */; };\n\t\t03FA318C2C6FECAE00D47FA3 /* Flow in Frameworks */ = {isa = PBXBuildFile; productRef = 03FA318B2C6FECAE00D47FA3 /* Flow */; };\n\t\t03FA318F2C6FEF2000D47FA3 /* InteractionBarActionLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA318E2C6FEF2000D47FA3 /* InteractionBarActionLabelView.swift */; };\n\t\t03FD6CB02C9B719100500FD6 /* View+PopupAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FD6CAF2C9B719100500FD6 /* View+PopupAnchor.swift */; };\n\t\t03FE14042BF93FDD00A8377F /* ErrorDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FE14032BF93FDD00A8377F /* ErrorDetails.swift */; };\n\t\t03FE14082BF94FFB00A8377F /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FE14072BF94FFB00A8377F /* ErrorView.swift */; };\n\t\t03FE140C2BF953B000A8377F /* HandleError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FE140B2BF953B000A8377F /* HandleError.swift */; };\n\t\t50C99B562A61D792005D57DD /* Dependencies in Frameworks */ = {isa = PBXBuildFile; productRef = 50C99B552A61D792005D57DD /* Dependencies */; };\n\t\t6332FDBD27EFAF7C0009A98A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 6332FDBC27EFAF7B0009A98A /* Settings.bundle */; };\n\t\t636250DC2A18111400FC59B4 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 636250DB2A18111400FC59B4 /* KeychainAccess */; };\n\t\t6363D5C527EE196700E34822 /* MlemApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6363D5C427EE196700E34822 /* MlemApp.swift */; };\n\t\t6363D5C727EE196700E34822 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6363D5C627EE196700E34822 /* ContentView.swift */; };\n\t\t6363D5C927EE196A00E34822 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6363D5C827EE196A00E34822 /* Assets.xcassets */; };\n\t\t6DE1183C2A4A217400810C7E /* Profile View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DE1183B2A4A217400810C7E /* Profile View.swift */; };\n\t\t814CEF632F44577A0090F812 /* HiddenReadBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814CEF622F44577A0090F812 /* HiddenReadBannerView.swift */; };\n\t\t81A179BC2DDE591700B17017 /* LongPressActionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A179BB2DDE591700B17017 /* LongPressActionSettingsView.swift */; };\n\t\t81A179BF2DDE5BF300B17017 /* TabBarLongPressAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A179BE2DDE5BF300B17017 /* TabBarLongPressAction.swift */; };\n\t\t81C4B4332F493C5E001406A1 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 81C4B4322F493C5E001406A1 /* InfoPlist.xcstrings */; };\n\t\t81DE61C52F48AF44006E4C36 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81DE61C42F48AF44006E4C36 /* UniformTypeIdentifiers.framework */; };\n\t\t81DE61D02F48AF44006E4C36 /* OpenInMlem.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 81DE61C32F48AF44006E4C36 /* OpenInMlem.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };\n\t\t81DE61DB2F48AF4C006E4C36 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = 81DE61D62F48AF4C006E4C36 /* Action.js */; };\n\t\t81DE61DD2F48AF4C006E4C36 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 81DE61D92F48AF4C006E4C36 /* Media.xcassets */; };\n\t\t81DE61DE2F48AF4C006E4C36 /* ActionRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81DE61D72F48AF4C006E4C36 /* ActionRequestHandler.swift */; };\n\t\tAD1B0D372A5F7A260006F554 /* Licenses.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD1B0D362A5F7A260006F554 /* Licenses.swift */; };\n\t\tB104A6D82A59BF3C00B3E725 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = B104A6D72A59BF3C00B3E725 /* Nuke */; };\n\t\tB104A6DA2A59BF3C00B3E725 /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = B104A6D92A59BF3C00B3E725 /* NukeExtensions */; };\n\t\tB104A6DC2A59BF3C00B3E725 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = B104A6DB2A59BF3C00B3E725 /* NukeUI */; };\n\t\tB104A6DE2A59BF3C00B3E725 /* NukeVideo in Frameworks */ = {isa = PBXBuildFile; productRef = B104A6DD2A59BF3C00B3E725 /* NukeVideo */; };\n\t\tB1B78D642A51D53900F72485 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B78D632A51D53900F72485 /* AppDelegate.swift */; };\n\t\tCD03742A2D1DBFD2001E85FA /* ReadCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0374292D1DBFCF001E85FA /* ReadCheck.swift */; };\n\t\tCD03B5BE2F3BA16400AEF786 /* Blockable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD03B5BD2F3BA16100AEF786 /* Blockable+Extensions.swift */; };\n\t\tCD0C5C102E99629A0074D5A4 /* ExportablePostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0C5C0F2E9962970074D5A4 /* ExportablePostEditorView.swift */; };\n\t\tCD0E06F72C0E739F00445849 /* PostType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0E06F62C0E739F00445849 /* PostType+Extensions.swift */; };\n\t\tCD0F280A2C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0F28092C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift */; };\n\t\tCD10FA772C7A8622008985AD /* ImageSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD10FA762C7A8622008985AD /* ImageSaver.swift */; };\n\t\tCD13CC592C583C7A001AF428 /* WebsitePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD13CC582C583C7A001AF428 /* WebsitePreviewView.swift */; };\n\t\tCD13CC5B2C588B34001AF428 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD13CC5A2C588B34001AF428 /* WebView.swift */; };\n\t\tCD13CC652C5D2B9D001AF428 /* CircleCroppedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD13CC642C5D2B9D001AF428 /* CircleCroppedImageView.swift */; };\n\t\tCD1446212A5B328E00610EF1 /* Privacy Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1446202A5B328E00610EF1 /* Privacy Policy.swift */; };\n\t\tCD1446252A5B357900610EF1 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1446242A5B357900610EF1 /* Document.swift */; };\n\t\tCD1446272A5B36DA00610EF1 /* EULA.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1446262A5B36DA00610EF1 /* EULA.swift */; };\n\t\tCD1B2E212C7F84160075C7EA /* View+MarkReadOnScroll.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B2E202C7F84160075C7EA /* View+MarkReadOnScroll.swift */; };\n\t\tCD1C64152D3428710006B3C1 /* CommunityView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1C64142D34286E0006B3C1 /* CommunityView+Logic.swift */; };\n\t\tCD1D31832C56D742001B434B /* View+WidthReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1D31822C56D742001B434B /* View+WidthReader.swift */; };\n\t\tCD1DF6382D38357500F7851E /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1DF6372D38357100F7851E /* MediaView.swift */; };\n\t\tCD1DF63E2D387E8500F7851E /* MediaView+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1DF63D2D387E8300F7851E /* MediaView+Views.swift */; };\n\t\tCD24CAFC2D5568FE0032B5E8 /* DiscussionLanguageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD24CAFB2D5568F90032B5E8 /* DiscussionLanguageSettingsView.swift */; };\n\t\tCD27839A2D9B366000DD4C69 /* ZoomableImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2783992D9B365D00DD4C69 /* ZoomableImageView.swift */; };\n\t\tCD2C86542D5556C00034CD8A /* MlemError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2C86532D5556BE0034CD8A /* MlemError.swift */; };\n\t\tCD3153072C38421B00BC5FBE /* View+LoadFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3153062C38421B00BC5FBE /* View+LoadFeed.swift */; };\n\t\tCD332D792CA7175500A53988 /* PlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD332D782CA7175200A53988 /* PlayButton.swift */; };\n\t\tCD332D7E2CA7486000A53988 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD332D7D2CA7485D00A53988 /* String+Extensions.swift */; };\n\t\tCD33CA522D3C18BF00106C8C /* ImageViewer+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD33CA512D3C18AE00106C8C /* ImageViewer+Views.swift */; };\n\t\tCD3485BB2D501470006748B8 /* ZoomSliderLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3485BA2D501463006748B8 /* ZoomSliderLocation.swift */; };\n\t\tCD3485BD2D501573006748B8 /* ZoomSliderSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3485BC2D50156E006748B8 /* ZoomSliderSettingsView.swift */; };\n\t\tCD3FC6802D4A75090088E63B /* CounterApperance+StaticValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3FC67F2D4A75050088E63B /* CounterApperance+StaticValues.swift */; };\n\t\tCD4368C12AE23FD400BD8BD1 /* Semaphore in Frameworks */ = {isa = PBXBuildFile; productRef = CD4368C02AE23FD400BD8BD1 /* Semaphore */; };\n\t\tCD43E8B32BF2C24E007C3D71 /* ContentLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD43E8B22BF2C24E007C3D71 /* ContentLoader.swift */; };\n\t\tCD44C92C2E5CC8B900F24AC8 /* ConditionalLabelStyleViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD44C92B2E5CC8B600F24AC8 /* ConditionalLabelStyleViewModifier.swift */; };\n\t\tCD45CB0D2D1880E8008BC729 /* FiltersSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD45CB0C2D1880E3008BC729 /* FiltersSettingsView.swift */; };\n\t\tCD4B66DA2DCE809D00D28EB4 /* InstanceUptimeView+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4B66D92DCE809A00D28EB4 /* InstanceUptimeView+Views.swift */; };\n\t\tCD4BAD352B4B2C0B00A1E726 /* FeedsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */; };\n\t\tCD4D583F2B86855F00B82964 /* MlemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D583E2B86855F00B82964 /* MlemTests.swift */; };\n\t\tCD4D58412B86858100B82964 /* MlemUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58402B86858100B82964 /* MlemUITests.swift */; };\n\t\tCD4D58A52B86BD1B00B82964 /* PersistenceRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */; };\n\t\tCD4D58AA2B86BE5900B82964 /* AccountListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58A92B86BE5900B82964 /* AccountListView.swift */; };\n\t\tCD4D58AD2B86BE7100B82964 /* QuickSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58AC2B86BE7100B82964 /* QuickSwitcherView.swift */; };\n\t\tCD4D58B32B86BFD400B82964 /* AccountsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58B22B86BFD400B82964 /* AccountsTracker.swift */; };\n\t\tCD4D58B52B86BFFB00B82964 /* PersistenceRepository+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58B42B86BFFA00B82964 /* PersistenceRepository+Dependency.swift */; };\n\t\tCD4D58B92B86D9F800B82964 /* AccountListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58B82B86D9F800B82964 /* AccountListRow.swift */; };\n\t\tCD4D58BB2B86DA7D00B82964 /* AccountListView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58BA2B86DA7D00B82964 /* AccountListView+Logic.swift */; };\n\t\tCD4D58C82B86DCED00B82964 /* AvatarType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58C72B86DCED00B82964 /* AvatarType.swift */; };\n\t\tCD4D58CF2B86DDEC00B82964 /* AccountSortMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58CE2B86DDEC00B82964 /* AccountSortMode.swift */; };\n\t\tCD4D58EB2B86E63300B82964 /* AssociatedColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58EA2B86E63300B82964 /* AssociatedColor.swift */; };\n\t\tCD4D58F82B87B0D100B82964 /* InternetSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58F72B87B0D100B82964 /* InternetSpeed.swift */; };\n\t\tCD4D59142B87B36B00B82964 /* InternetConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D59132B87B36B00B82964 /* InternetConnectionManager.swift */; };\n\t\tCD4D59162B87B38C00B82964 /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D59152B87B38C00B82964 /* UIApplication+Extensions.swift */; };\n\t\tCD4D59182B87B3B000B82964 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D59172B87B3B000B82964 /* UIViewController+Extensions.swift */; };\n\t\tCD4E386D2C836F8C009B24F2 /* UIUserInterfaceStyle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4E386C2C836F8C009B24F2 /* UIUserInterfaceStyle+Extensions.swift */; };\n\t\tCD4ED8472BF110FA00EFA0A2 /* TabReselectTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */; };\n\t\tCD4ED84A2BF1113800EFA0A2 /* View+TabReselectConsumer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4ED8492BF1113800EFA0A2 /* View+TabReselectConsumer.swift */; };\n\t\tCD5581DE2C7B8B820043FAC3 /* ImageFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD5581DD2C7B8B820043FAC3 /* ImageFunctions.swift */; };\n\t\tCD57AFA52D0377EB00AB3956 /* AnimationControlLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD57AFA42D0377E400AB3956 /* AnimationControlLayer.swift */; };\n\t\tCD5C197D2D97096F0089614C /* ZoomCurves.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD5C197C2D97096D0089614C /* ZoomCurves.swift */; };\n\t\tCD5C197F2D970E5F0089614C /* MomentumStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD5C197E2D970E570089614C /* MomentumStatus.swift */; };\n\t\tCD5CAA0C2D41AE57008E20F2 /* EmbeddingSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD5CAA0B2D41AE52008E20F2 /* EmbeddingSettingsView.swift */; };\n\t\tCD5D8E2B2D6D5AB100AF5CE4 /* CGSize+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD5D8E2A2D6D5AAE00AF5CE4 /* CGSize+Extensions.swift */; };\n\t\tCD635E1B2C94DACD00864F75 /* BypassProxyWarningSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD635E1A2C94DACD00864F75 /* BypassProxyWarningSheet.swift */; };\n\t\tCD6436502D483C96002668FB /* InteractionBarEditorView+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD64364F2D483C8F002668FB /* InteractionBarEditorView+Views.swift */; };\n\t\tCD6DC02A2D86513D00693B16 /* AnimatedAvatarBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6DC0292D86513800693B16 /* AnimatedAvatarBehavior.swift */; };\n\t\tCD6DC02C2D86540A00693B16 /* AnimatedAvatarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6DC02B2D86540500693B16 /* AnimatedAvatarSettingsView.swift */; };\n\t\tCD737FBA2F771BF600E46411 /* InstanceSummarySoftware+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD737FB92F771BF100E46411 /* InstanceSummarySoftware+Extensions.swift */; };\n\t\tCD756A962ED765EC0031D7D1 /* ExportableCommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD756A952ED765E90031D7D1 /* ExportableCommentView.swift */; };\n\t\tCD756A982ED7669C0031D7D1 /* ExportableCommentEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD756A972ED766930031D7D1 /* ExportableCommentEditorView.swift */; };\n\t\tCD77437F2C1BA5CE0085BB43 /* MultiplatformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD77437E2C1BA5CE0085BB43 /* MultiplatformView.swift */; };\n\t\tCD7743892C20EDEE0085BB43 /* VotesModel+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7743882C20EDEE0085BB43 /* VotesModel+Extensions.swift */; };\n\t\tCD7882A72BFFD1A3002E1A30 /* ThumbnailLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7882A62BFFD1A3002E1A30 /* ThumbnailLocation.swift */; };\n\t\tCD7882A92BFFDFC7002E1A30 /* PostTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7882A82BFFDFC7002E1A30 /* PostTag.swift */; };\n\t\tCD7882AB2C013005002E1A30 /* EllipsisMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7882AA2C013005002E1A30 /* EllipsisMenu.swift */; };\n\t\tCD79281F2C73B52A00FA712D /* EndOfFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD79281E2C73B52A00FA712D /* EndOfFeedView.swift */; };\n\t\tCD7928232C73CBA400FA712D /* TileScoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7928222C73CBA400FA712D /* TileScoreView.swift */; };\n\t\tCD7928262C73E73400FA712D /* PersonView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7928252C73E73400FA712D /* PersonView+Logic.swift */; };\n\t\tCD7AC2CC2D7FDFFB00A671B7 /* MediaView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7AC2CB2D7FDFF700A671B7 /* MediaView+Logic.swift */; };\n\t\tCD7BF9322D18F4ED0020F2C5 /* FiltersTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7BF9312D18F4EB0020F2C5 /* FiltersTracker.swift */; };\n\t\tCD7C4E572F3B92BA00ADCBDD /* View+ReloadOnAccountSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7C4E562F3B92A600ADCBDD /* View+ReloadOnAccountSwitch.swift */; };\n\t\tCD7DB9712C49C17200DCC542 /* PersonContentGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7DB9702C49C17200DCC542 /* PersonContentGridView.swift */; };\n\t\tCD7DB9732C4AEDDE00DCC542 /* TileCommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7DB9722C4AEDDE00DCC542 /* TileCommentView.swift */; };\n\t\tCD7DB9762C4D6C0A00DCC542 /* FeedCommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7DB9752C4D6C0A00DCC542 /* FeedCommentView.swift */; };\n\t\tCD869FCC2C15F8AC00FC8B5B /* BubblePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD869FCB2C15F8AC00FC8B5B /* BubblePickerView.swift */; };\n\t\tCD869FCE2C15F90C00FC8B5B /* ChildSizeReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD869FCD2C15F90C00FC8B5B /* ChildSizeReader.swift */; };\n\t\tCD87BEC32D5132BE0099F190 /* FilterViolationWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD87BEC22D5132BB0099F190 /* FilterViolationWarning.swift */; };\n\t\tCD8DB93A2D22004000EB0C7B /* Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8DB9392D22003D00EB0C7B /* Animations.swift */; };\n\t\tCD93420B2DCD069800945333 /* InstanceUptimeView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD93420A2DCD069800945333 /* InstanceUptimeView+Logic.swift */; };\n\t\tCD950E0F2F0ED6F7002A0595 /* FeedPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD950E042F0ED6F7002A0595 /* FeedPostView.swift */; };\n\t\tCD9857A42C5E7F9D0084C71F /* NsfwOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9857A32C5E7F9D0084C71F /* NsfwOverlayView.swift */; };\n\t\tCD9BD5812D8F8F7D0006AB7F /* ZoomRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9BD5802D8F8F750006AB7F /* ZoomRecognizer.swift */; };\n\t\tCD9CFA2A2C2E1E8400739BBC /* FeedHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9CFA292C2E1E8400739BBC /* FeedHeaderView.swift */; };\n\t\tCD9CFA2C2C2E1EF300739BBC /* FeedIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9CFA2B2C2E1EF300739BBC /* FeedIconView.swift */; };\n\t\tCD9CFA302C2E22F600739BBC /* FeedDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9CFA2F2C2E22F600739BBC /* FeedDescription.swift */; };\n\t\tCD9CFA372C306DAB00739BBC /* PostGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9CFA362C306DAB00739BBC /* PostGridView.swift */; };\n\t\tCD9D243D2CC1DF59006E5F3F /* AccountType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9D243C2CC1DF55006E5F3F /* AccountType.swift */; };\n\t\tCDA1D2A52ED8C46B0077A9EA /* ExportableViewComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA1D2A42ED8C4670077A9EA /* ExportableViewComponents.swift */; };\n\t\tCDA67A9F2D9B32A100E5D17B /* CGPoint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA67A9E2D9B329D00E5D17B /* CGPoint+Extensions.swift */; };\n\t\tCDA683F82C77E577000C4486 /* NsfwBlurBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA683F72C77E577000C4486 /* NsfwBlurBehavior.swift */; };\n\t\tCDA711FB2DB5CAC3008BC3ED /* Media in Frameworks */ = {isa = PBXBuildFile; productRef = CDA711FA2DB5CAC3008BC3ED /* Media */; };\n\t\tCDAA02DB2C810DB200D75633 /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAA02DA2C810DB200D75633 /* Calendar+Extensions.swift */; };\n\t\tCDAA02E12C817AAB00D75633 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAA02E02C817AAB00D75633 /* Color+Extensions.swift */; };\n\t\tCDAA02E32C821C9100D75633 /* Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAA02E22C821C9100D75633 /* Divider.swift */; };\n\t\tCDADDB272F49210A00A4214A /* CommunityStubResolutionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDADDB262F49210A00A4214A /* CommunityStubResolutionPage.swift */; };\n\t\tCDB2EC7D2BFADAB300DBC0EF /* CompactPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB2EC7C2BFADAB300DBC0EF /* CompactPostView.swift */; };\n\t\tCDB2EC7F2BFADACC00DBC0EF /* HeadlinePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB2EC7E2BFADACC00DBC0EF /* HeadlinePostView.swift */; };\n\t\tCDB2EC812BFADADF00DBC0EF /* LargePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB2EC802BFADADF00DBC0EF /* LargePostView.swift */; };\n\t\tCDB2EC862BFADD7C00DBC0EF /* FullyQualifiedLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB2EC852BFADD7C00DBC0EF /* FullyQualifiedLabelView.swift */; };\n\t\tCDB2EC882BFAE14800DBC0EF /* FullyQualifiedNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB2EC872BFAE14800DBC0EF /* FullyQualifiedNameView.swift */; };\n\t\tCDB2EC8A2BFAEFDF00DBC0EF /* ThumbnailImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB2EC892BFAEFDF00DBC0EF /* ThumbnailImageView.swift */; };\n\t\tCDB3DDFD2DA485D200F407AB /* SettingsValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB3DDFC2DA485D000F407AB /* SettingsValues.swift */; };\n\t\tCDB41E8A2C83C24400BD2DE9 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB41E892C83C24400BD2DE9 /* Section.swift */; };\n\t\tCDB738292CB8A6A5005B11BB /* View+PaletteBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB738282CB8A69D005B11BB /* View+PaletteBorder.swift */; };\n\t\tCDB95FCD2EBD3230008669D9 /* SmallOverlayButtonLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB95FCC2EBD3212008669D9 /* SmallOverlayButtonLabel.swift */; };\n\t\tCDBE78C22F38DD8D008B254C /* PersonStubResolutionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBE78C12F38DD89008B254C /* PersonStubResolutionPage.swift */; };\n\t\tCDBEC30F2E84C9F800F30B00 /* ExportablePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBEC30E2E84C9F300F30B00 /* ExportablePostView.swift */; };\n\t\tCDBFCB652C03920C008CD468 /* PostLinkHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBFCB642C03920C008CD468 /* PostLinkHostView.swift */; };\n\t\tCDBFCB6A2C04EFFE008CD468 /* PostSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBFCB692C04EFFE008CD468 /* PostSettingsView.swift */; };\n\t\tCDBFCB6C2C054AA7008CD468 /* TilePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDBFCB6B2C054AA7008CD468 /* TilePostView.swift */; };\n\t\tCDC44A362D1CBC280030F01C /* ReadPostIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC44A352D1CBC200030F01C /* ReadPostIndicator.swift */; };\n\t\tCDCA44B42C176A4700C092B3 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCA44B32C176A4700C092B3 /* Array+Extensions.swift */; };\n\t\tCDCC1BA72D99BDB7006579DF /* GestureRecognizers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCC1BA62D99BDB0006579DF /* GestureRecognizers.swift */; };\n\t\tCDCC1BA92D99BEC1006579DF /* ZoomRecognizerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCC1BA82D99BEA8006579DF /* ZoomRecognizerCoordinator.swift */; };\n\t\tCDCC7FED2D9AEDC800CE18DA /* BridgeDragValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCC7FEC2D9AEDBE00CE18DA /* BridgeDragValue.swift */; };\n\t\tCDCC7FEF2D9B1E7100CE18DA /* CachedComputation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCC7FEE2D9B1E6E00CE18DA /* CachedComputation.swift */; };\n\t\tCDCC7FF12D9B27D800CE18DA /* ZoomRecognizerCoordinator+GestureRecognition.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCC7FF02D9B27CE00CE18DA /* ZoomRecognizerCoordinator+GestureRecognition.swift */; };\n\t\tCDCC7FF32D9B283A00CE18DA /* ZoomRecognizerCoordinator+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCC7FF22D9B283400CE18DA /* ZoomRecognizerCoordinator+Logic.swift */; };\n\t\tCDCF5A582F1426A5006748E8 /* CommentStubResolutionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCF5A572F1426A0006748E8 /* CommentStubResolutionPage.swift */; };\n\t\tCDD4A09C2C8A122F0001AD1A /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD4A09B2C8A122F0001AD1A /* Settings.swift */; };\n\t\tCDD4A09E2C8B69FC0001AD1A /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD4A09D2C8B69FC0001AD1A /* Button.swift */; };\n\t\tCDD4A0A02C8B985D0001AD1A /* ImportExportSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD4A09F2C8B985D0001AD1A /* ImportExportSettingsView.swift */; };\n\t\tCDD8B94C2C8234BC00510EBB /* Form.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD8B94B2C8234BC00510EBB /* Form.swift */; };\n\t\tCDD8E30D2EEA07F100FC4C8D /* ExportableCommentLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD8E30C2EEA07EE00FC4C8D /* ExportableCommentLoader.swift */; };\n\t\tCDD99C3C2C73F3FF0010367F /* DeleteAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD99C3B2C73F3FF0010367F /* DeleteAccountView.swift */; };\n\t\tCDD99C3E2C73F4380010367F /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD99C3D2C73F4380010367F /* WarningView.swift */; };\n\t\tCDDA49A82F7044D2004A5AFF /* InstanceStubResolutionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDA49A72F7044CD004A5AFF /* InstanceStubResolutionPage.swift */; };\n\t\tCDE1F18F2C63D75A008AF042 /* LegacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE1F18E2C63D75A008AF042 /* LegacySettings.swift */; };\n\t\tCDE1F1942C63DF44008AF042 /* PlatformConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE1F1932C63DF44008AF042 /* PlatformConstants.swift */; };\n\t\tCDE1F1962C63DF89008AF042 /* PhoneConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE1F1952C63DF89008AF042 /* PhoneConstants.swift */; };\n\t\tCDE1F1982C63DFC9008AF042 /* PadConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE1F1972C63DFC9008AF042 /* PadConstants.swift */; };\n\t\tCDE1F19C2C63E2EB008AF042 /* SettingPropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE1F19B2C63E2EB008AF042 /* SettingPropertyWrapper.swift */; };\n\t\tCDE1F19E2C63E306008AF042 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE1F19D2C63E306008AF042 /* Constants.swift */; };\n\t\tCDE4AC472CA372B600981010 /* SDWebImageWebPCoder in Frameworks */ = {isa = PBXBuildFile; productRef = CDE4AC462CA372B600981010 /* SDWebImageWebPCoder */; };\n\t\tCDE88C352E68938D00183AE5 /* View+AccountSwitcherGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE88C342E68938800183AE5 /* View+AccountSwitcherGesture.swift */; };\n\t\tCDEE15522D22190600EB9D7B /* ErrorsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEE15512D22190000EB9D7B /* ErrorsTracker.swift */; };\n\t\tCDEE15542D22364B00EB9D7B /* ErrorLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEE15532D22364500EB9D7B /* ErrorLogView.swift */; };\n\t\tCDF60A012E998BB5005FA3F1 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF60A002E998BB2005FA3F1 /* Data+Extensions.swift */; };\n\t\tCDF8C1912D5D502400295CBA /* InteractionBarWidgetPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF8C1902D5D501E00295CBA /* InteractionBarWidgetPickerView.swift */; };\n\t\tCDF9EF332AB2845C003F885B /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF9EF322AB2845C003F885B /* Icons.swift */; };\n\t\tCDFB8C692C7796020070845F /* View+DynamicBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFB8C682C7796020070845F /* View+DynamicBlur.swift */; };\n\t\tCDFF9A332D88C3C1009E02E2 /* CGFloat+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFF9A322D88C3BE009E02E2 /* CGFloat+Extensions.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t6363D5D727EE196A00E34822 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 6363D5B927EE196700E34822 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 6363D5C027EE196700E34822;\n\t\t\tremoteInfo = Mlem;\n\t\t};\n\t\t6363D5E127EE196A00E34822 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 6363D5B927EE196700E34822 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 6363D5C027EE196700E34822;\n\t\t\tremoteInfo = Mlem;\n\t\t};\n\t\t81DE61CE2F48AF44006E4C36 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 6363D5B927EE196700E34822 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 81DE61C22F48AF44006E4C36;\n\t\t\tremoteInfo = OpenInMlem;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t81DE61D12F48AF44006E4C36 /* Embed Foundation Extensions */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 13;\n\t\t\tfiles = (\n\t\t\t\t81DE61D02F48AF44006E4C36 /* OpenInMlem.appex in Embed Foundation Extensions */,\n\t\t\t);\n\t\t\tname = \"Embed Foundation Extensions\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t0300309C2C4163C9009A65FF /* CommentTreeTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentTreeTracker.swift; sourceTree = \"<group>\"; };\n\t\t030030A02C416B0B009A65FF /* RefreshPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshPopupView.swift; sourceTree = \"<group>\"; };\n\t\t030050D22D109B7E002B1E99 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = \"<group>\"; };\n\t\t030050D42D10AE30002B1E99 /* Report+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Report+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t030056A32D7DB05A00EB0BA3 /* SharingLinksSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingLinksSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t030056A52D7DBD4F00EB0BA3 /* Sharable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Sharable+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t030056BA2D7E137800EB0BA3 /* ShareInstancePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareInstancePickerView.swift; sourceTree = \"<group>\"; };\n\t\t0302A87F2F9BC379000A127A /* SearchHomeCategoryLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeCategoryLabelStyle.swift; sourceTree = \"<group>\"; };\n\t\t03036C732C71408700C6DA1D /* CounterAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterAppearance.swift; sourceTree = \"<group>\"; };\n\t\t03036C822C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InteractionBarEditorView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t03049A192C6502F300FF6889 /* FormSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormSection.swift; sourceTree = \"<group>\"; };\n\t\t03049A1B2C65039400FF6889 /* ActiveUserCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveUserCountView.swift; sourceTree = \"<group>\"; };\n\t\t03049A1D2C6508F400FF6889 /* RegistrationMode+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"RegistrationMode+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03049A1F2C650A8100FF6889 /* FormReadout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormReadout.swift; sourceTree = \"<group>\"; };\n\t\t03049A212C650B2C00FF6889 /* CommunityDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityDetailsView.swift; sourceTree = \"<group>\"; };\n\t\t0305EBA92D32B3B80066E5AD /* ModlogView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ModlogView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t0305EBAB2D32C9300066E5AD /* ModlogEntryType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ModlogEntryType+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0305EBB12D35C1B70066E5AD /* RegistrationApplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationApplicationView.swift; sourceTree = \"<group>\"; };\n\t\t030778EB2C52ED350018E61C /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = \"<group>\"; };\n\t\t030BCB1A2C3EA5FD0037680F /* InstanceDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceDetailsView.swift; sourceTree = \"<group>\"; };\n\t\t030E95E62C80A20A0045BC2C /* View+NavigationTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+NavigationTransition.swift\"; sourceTree = \"<group>\"; };\n\t\t030EE3032D651A4100D58C2C /* View+Refreshable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+Refreshable.swift\"; sourceTree = \"<group>\"; };\n\t\t030FF67A2BC8521600F6BFAC /* CustomTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTabView.swift; sourceTree = \"<group>\"; };\n\t\t030FF67C2BC8524500F6BFAC /* CustomTabItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTabItem.swift; sourceTree = \"<group>\"; };\n\t\t030FF67E2BC8544600F6BFAC /* CustomTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTabBarController.swift; sourceTree = \"<group>\"; };\n\t\t030FF6802BC859FD00F6BFAC /* CustomTabViewHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTabViewHostingController.swift; sourceTree = \"<group>\"; };\n\t\t0311ADB62E4DF49800EC3120 /* SearchHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeView.swift; sourceTree = \"<group>\"; };\n\t\t0311ADB82E4E0E0800EC3120 /* VisitAgainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitAgainView.swift; sourceTree = \"<group>\"; };\n\t\t0311ADBC2E4F668900EC3120 /* TopCommunitiesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopCommunitiesListView.swift; sourceTree = \"<group>\"; };\n\t\t0311ADBE2E4F68D000EC3120 /* TopPeopleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopPeopleListView.swift; sourceTree = \"<group>\"; };\n\t\t0311ADC02E4F693B00EC3120 /* TopInstancesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopInstancesListView.swift; sourceTree = \"<group>\"; };\n\t\t03134A4F2BEAD245002662CC /* NavigationLink+NavigationPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"NavigationLink+NavigationPage.swift\"; sourceTree = \"<group>\"; };\n\t\t03134A512BEAD69F002662CC /* SettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPage.swift; sourceTree = \"<group>\"; };\n\t\t03134A572BEC1C46002662CC /* AccountListSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03134A592BEC2253002662CC /* AvatarStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackView.swift; sourceTree = \"<group>\"; };\n\t\t0315B1BD2C74C3D6006D4F82 /* CommentEditorView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CommentEditorView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t0315B1C02C74C71A006D4F82 /* CommentEditorView+Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CommentEditorView+Context.swift\"; sourceTree = \"<group>\"; };\n\t\t0315B1C52C754802006D4F82 /* PostEditorView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PostEditorView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t0316CD632C382A6A009EA8EA /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = \"<group>\"; };\n\t\t0318BA9E2D72405F006CA71F /* PostSortType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PostSortType+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t031CA0D82E4FBFD800CF0C0F /* MarkAllAsReadButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAllAsReadButton.swift; sourceTree = \"<group>\"; };\n\t\t031CA4AD2E58A84E00CF0C0F /* View+WithSheetSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+WithSheetSearch.swift\"; sourceTree = \"<group>\"; };\n\t\t031CA5742E5900C800CF0C0F /* SwipeConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SwipeConfiguration+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t031CA5B42E590B0D00CF0C0F /* QuickSwipeAction+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"QuickSwipeAction+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t031CA5B62E599F7E00CF0C0F /* View+QuickSwipes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+QuickSwipes.swift\"; sourceTree = \"<group>\"; };\n\t\t031DBA672F9A65DC00B4BAE4 /* BackendClient+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"BackendClient+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t031DBA762F9A93AD00B4BAE4 /* EventRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventRowView.swift; sourceTree = \"<group>\"; };\n\t\t031DBA782F9A95B200B4BAE4 /* SearchHomeLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeLabelStyle.swift; sourceTree = \"<group>\"; };\n\t\t031DBA7A2F9A993000B4BAE4 /* SearchHomeListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeListView.swift; sourceTree = \"<group>\"; };\n\t\t031E2D502BEF961D0003BC45 /* SubscriptionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionListView.swift; sourceTree = \"<group>\"; };\n\t\t031E2D5A2BEFC9460003BC45 /* ThemeSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t031E2D5C2BEFCC630003BC45 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = \"<group>\"; };\n\t\t031EC52F2E5F77D7003408B7 /* FeedContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedContext.swift; sourceTree = \"<group>\"; };\n\t\t0320B64E2C8A638A00D38548 /* SignUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpView.swift; sourceTree = \"<group>\"; };\n\t\t0320B6532C8B65EB00D38548 /* Captcha+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Captcha+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0320B6572C8BB3C400D38548 /* SignUpView+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SignUpView+Views.swift\"; sourceTree = \"<group>\"; };\n\t\t0320B6592C8CBB0D00D38548 /* SignUpView+EmailConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SignUpView+EmailConfirmationView.swift\"; sourceTree = \"<group>\"; };\n\t\t0320B65B2C8CFBDC00D38548 /* ImageUploadHistoryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadHistoryManager.swift; sourceTree = \"<group>\"; };\n\t\t0320B6602C8DFCF100D38548 /* SearchView+FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SearchView+FiltersView.swift\"; sourceTree = \"<group>\"; };\n\t\t0320B6622C8F8D5A00D38548 /* InstanceSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSort.swift; sourceTree = \"<group>\"; };\n\t\t0320B6642C91DBD500D38548 /* NavigationPage+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = \"NavigationPage+View.swift\"; sourceTree = \"<group>\"; };\n\t\t0320B6662C93504600D38548 /* SignUpView+Logic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = \"SignUpView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t0320B6682C93506300D38548 /* SearchView+Logic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = \"SearchView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t0324FA762C1F0AE100F6247D /* Readout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readout.swift; sourceTree = \"<group>\"; };\n\t\t0324FA7A2C1F2CD200F6247D /* InfoStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoStackView.swift; sourceTree = \"<group>\"; };\n\t\t0325B9392D3A9E8100E28B97 /* InboxBadgeSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxBadgeSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t0325B93B2D3AA62500E28B97 /* InboxItemType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InboxItemType+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0325B93D2D3AAE9E00E28B97 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = \"<group>\"; };\n\t\t03267D812BED489C009D6268 /* AvatarBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarBannerView.swift; sourceTree = \"<group>\"; };\n\t\t03267D832BED49CE009D6268 /* AccountSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t032A22002EB2A4D100E8B346 /* PersonContentFeedLoader+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PersonContentFeedLoader+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t032C32032C3439C600595286 /* ActorIdentifiable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ActorIdentifiable+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t032C32072C34469900595286 /* SelectableContentProviding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SelectableContentProviding+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t032C32092C34495D00595286 /* SelectTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTextView.swift; sourceTree = \"<group>\"; };\n\t\t032C32152C36F65500595286 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = \"<group>\"; };\n\t\t032C32172C36F70300595286 /* ReplyBarConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyBarConfiguration.swift; sourceTree = \"<group>\"; };\n\t\t0331715D2CCD6D95002DA370 /* ContentPurgeEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPurgeEditorView.swift; sourceTree = \"<group>\"; };\n\t\t033171772CCE89E3002DA370 /* PurgableProviding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PurgableProviding+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0335AE102D8991330094FFD9 /* View+HiddenNavigationTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+HiddenNavigationTitle.swift\"; sourceTree = \"<group>\"; };\n\t\t033819272D4424D9000AFC55 /* SafetyWarningsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafetyWarningsSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t033EF40F2CB9AEF7004D8A3F /* ExpandedPostView+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ExpandedPostView+Views.swift\"; sourceTree = \"<group>\"; };\n\t\t033F84482D18D1F400D87A9E /* MessageFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageFeedView.swift; sourceTree = \"<group>\"; };\n\t\t033F844C2D18D90900D87A9E /* MessageBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubbleView.swift; sourceTree = \"<group>\"; };\n\t\t033F84502D196AFD00D87A9E /* MessageFeedView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"MessageFeedView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t033F84652D1C780900D87A9E /* ModlogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModlogButtonView.swift; sourceTree = \"<group>\"; };\n\t\t033F84702D1C784600D87A9E /* ModlogEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModlogEntryView.swift; sourceTree = \"<group>\"; };\n\t\t033F84712D1C784600D87A9E /* ModlogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModlogView.swift; sourceTree = \"<group>\"; };\n\t\t033F84772D1D68D200D87A9E /* ModlogEntryContent+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ModlogEntryContent+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t033F84AC2C298466002E3EDF /* SectionIndexTitles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionIndexTitles.swift; sourceTree = \"<group>\"; };\n\t\t033F84B02C29907F002E3EDF /* FeedbackType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackType.swift; sourceTree = \"<group>\"; };\n\t\t033F84BA2C2ACB96002E3EDF /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = \"<group>\"; };\n\t\t033F84BC2C2ACC5F002E3EDF /* CommentBarConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBarConfiguration.swift; sourceTree = \"<group>\"; };\n\t\t033F84C02C2AD072002E3EDF /* CommentTreeNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentTreeNode.swift; sourceTree = \"<group>\"; };\n\t\t033F84C22C2B12AA002E3EDF /* InstanceSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSummary.swift; sourceTree = \"<group>\"; };\n\t\t033F84C72C2B193D002E3EDF /* MlemStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemStats.swift; sourceTree = \"<group>\"; };\n\t\t033F84CB2C2B1E46002E3EDF /* HandleThreadiverseLinksModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleThreadiverseLinksModifier.swift; sourceTree = \"<group>\"; };\n\t\t033FCAEB2C57DCCD007B7CD1 /* ListingType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ListingType+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t033FCAED2C57DD14007B7CD1 /* CaptchaDifficulty+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CaptchaDifficulty+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t033FCAF32C59843E007B7CD1 /* CommunityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityView.swift; sourceTree = \"<group>\"; };\n\t\t033FCB222C5E3933007B7CD1 /* AlternateIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlternateIcon.swift; sourceTree = \"<group>\"; };\n\t\t033FCB232C5E3933007B7CD1 /* AlternateIconCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlternateIconCell.swift; sourceTree = \"<group>\"; };\n\t\t033FCB242C5E3933007B7CD1 /* AlternateIconLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlternateIconLabel.swift; sourceTree = \"<group>\"; };\n\t\t033FCB252C5E3933007B7CD1 /* IconSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t033FCB3D2C5E7FA9007B7CD1 /* View+OutdatedFeedPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+OutdatedFeedPopup.swift\"; sourceTree = \"<group>\"; };\n\t\t034065C22D83742900637308 /* View+NavigtionStackPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+NavigtionStackPreview.swift\"; sourceTree = \"<group>\"; };\n\t\t034147FC2D8F5844005503AF /* ExpandedPostHistoryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandedPostHistoryTracker.swift; sourceTree = \"<group>\"; };\n\t\t0343C0472D3AD6DB001CF709 /* Set+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Set+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t034690922D0F4D720073E664 /* RemovableProviding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"RemovableProviding+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t034690982D105DFD0073E664 /* InboxView+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InboxView+Types.swift\"; sourceTree = \"<group>\"; };\n\t\t0348F98C2DDBB526006639CD /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = \"<group>\"; };\n\t\t034A82022EBA688F00E5F904 /* LinkEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkEditorView.swift; sourceTree = \"<group>\"; };\n\t\t034A85702EC0A1FA00E5F904 /* InstanceCommunityListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceCommunityListView.swift; sourceTree = \"<group>\"; };\n\t\t034B947E2C091EDD00039AF4 /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = \"<group>\"; };\n\t\t034B94802C09306D00039AF4 /* CommunityOrPersonStub+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CommunityOrPersonStub+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t034B94822C09340A00039AF4 /* MarkdownConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"MarkdownConfiguration+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t034B94882C09360A00039AF4 /* Int+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Int+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t034B948D2C0937BA00039AF4 /* FancyScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyScrollView.swift; sourceTree = \"<group>\"; };\n\t\t034CC02F2D22C5BE00C557D3 /* WarningOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WarningOverlayView.swift; sourceTree = \"<group>\"; };\n\t\t03500C212BF5594100CAA076 /* ToastType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastType.swift; sourceTree = \"<group>\"; };\n\t\t03500C232BF55D0E00CAA076 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = \"<group>\"; };\n\t\t03500C262BF69D1D00CAA076 /* ToastModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastModel.swift; sourceTree = \"<group>\"; };\n\t\t03500C2A2BF7F1B100CAA076 /* ToastOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastOverlayView.swift; sourceTree = \"<group>\"; };\n\t\t03500C2C2BF7FC2500CAA076 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = \"<group>\"; };\n\t\t03531EEB2C2D81DC004A3464 /* LinkSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03531EED2C2D9298004A3464 /* SearchSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSheetView.swift; sourceTree = \"<group>\"; };\n\t\t03531EF02C2DA298004A3464 /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = \"<group>\"; };\n\t\t03531EF42C2DA610004A3464 /* NavigationSearchType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSearchType.swift; sourceTree = \"<group>\"; };\n\t\t035394852C9CDC1100795AA5 /* SubscriptionListNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionListNavigationButton.swift; sourceTree = \"<group>\"; };\n\t\t0353948A2CA076D000795AA5 /* InboxView+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InboxView+Views.swift\"; sourceTree = \"<group>\"; };\n\t\t0353948C2CA080EB00795AA5 /* FeedWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedWelcomeView.swift; sourceTree = \"<group>\"; };\n\t\t0353948E2CA088E600795AA5 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t035394922CA1AE2C00795AA5 /* UptimeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UptimeData.swift; sourceTree = \"<group>\"; };\n\t\t035394942CA1AE6300795AA5 /* InstanceUptimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceUptimeView.swift; sourceTree = \"<group>\"; };\n\t\t035394982CA1B20B00795AA5 /* InstanceView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InstanceView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t0353949B2CA4B3E800795AA5 /* CrossPostListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossPostListView.swift; sourceTree = \"<group>\"; };\n\t\t0355F9452C150B2300605248 /* ExternalApiInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalApiInfoView.swift; sourceTree = \"<group>\"; };\n\t\t035BE0862BDD8DA000F77D73 /* NavigationRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootView.swift; sourceTree = \"<group>\"; };\n\t\t035BE0882BDD901B00F77D73 /* NavigationPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationPage.swift; sourceTree = \"<group>\"; };\n\t\t035BE08A2BDD903100F77D73 /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = \"<group>\"; };\n\t\t035BE08C2BDE88EC00F77D73 /* NavigationLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLayerView.swift; sourceTree = \"<group>\"; };\n\t\t035BE08E2BDE911900F77D73 /* NavigationLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLayer.swift; sourceTree = \"<group>\"; };\n\t\t035BE0902BDEA01E00F77D73 /* View+NavigationSheetModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+NavigationSheetModifiers.swift\"; sourceTree = \"<group>\"; };\n\t\t035DF9102EB2AA160021DE8C /* PersonContentGridView+FeedLoaderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PersonContentGridView+FeedLoaderType.swift\"; sourceTree = \"<group>\"; };\n\t\t035DF9122EB2AF8E0021DE8C /* GetContentFilter+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"GetContentFilter+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t035DFA222EB3F7240021DE8C /* CommunityAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityAboutView.swift; sourceTree = \"<group>\"; };\n\t\t035DFA242EB3FB550021DE8C /* CommunityDescriptionEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityDescriptionEditorView.swift; sourceTree = \"<group>\"; };\n\t\t035EDEEC2C2DE94B00F51144 /* _assignIfNotEqual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _assignIfNotEqual.swift; sourceTree = \"<group>\"; };\n\t\t035EDEED2C2DE94B00F51144 /* DefaultTextInputType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTextInputType.swift; sourceTree = \"<group>\"; };\n\t\t035EDEEE2C2DE94B00F51144 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = \"<group>\"; };\n\t\t035EDEEF2C2DE94B00F51144 /* SearchBar+NavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SearchBar+NavigationView.swift\"; sourceTree = \"<group>\"; };\n\t\t035EDEF02C2DE94B00F51144 /* SearchBarExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarExtensions.swift; sourceTree = \"<group>\"; };\n\t\t035EDEFA2C2DF98700F51144 /* CommunityListRowBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityListRowBody.swift; sourceTree = \"<group>\"; };\n\t\t035EDF002C2ECFE000F51144 /* Searchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Searchable.swift; sourceTree = \"<group>\"; };\n\t\t035EDF022C2ED0DE00F51144 /* PersonListRowBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonListRowBody.swift; sourceTree = \"<group>\"; };\n\t\t03600D922D4531DF00C704CB /* PrivacyBypassImageProxySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyBypassImageProxySettingsView.swift; sourceTree = \"<group>\"; };\n\t\t0368F3422D72796B007DEB70 /* LanguagePickerSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePickerSheetView.swift; sourceTree = \"<group>\"; };\n\t\t0368F34C2D733215007DEB70 /* SortTimeRange+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SortTimeRange+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0368F34E2D734066007DEB70 /* SearchSortType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SearchSortType+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0368F3682D7349D8007DEB70 /* LanguageListRowBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageListRowBody.swift; sourceTree = \"<group>\"; };\n\t\t0369B3522BFA514B001EFEDF /* ToastLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastLocation.swift; sourceTree = \"<group>\"; };\n\t\t0369B3552BFA6824001EFEDF /* InboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxView.swift; sourceTree = \"<group>\"; };\n\t\t0369B35A2BFB86E3001EFEDF /* Account.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = \"<group>\"; };\n\t\t036A84542D98253400E95D50 /* UpdateBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateBannerView.swift; sourceTree = \"<group>\"; };\n\t\t036A84D72D99531400E95D50 /* View+ConditionalNavigationTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+ConditionalNavigationTitle.swift\"; sourceTree = \"<group>\"; };\n\t\t036CC3AE2B8145C30098B6A1 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = \"<group>\"; };\n\t\t036ED6782D0AF3740018E5EA /* PinnedSortTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedSortTracker.swift; sourceTree = \"<group>\"; };\n\t\t036ED67A2D0B004D0018E5EA /* AdvancedSortView+SortButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"AdvancedSortView+SortButton.swift\"; sourceTree = \"<group>\"; };\n\t\t036ED67C2D0B006C0018E5EA /* TopSortPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopSortPicker.swift; sourceTree = \"<group>\"; };\n\t\t036ED67E2D0B9A520018E5EA /* CommunitySearchSortPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunitySearchSortPicker.swift; sourceTree = \"<group>\"; };\n\t\t036ED6822D0C483B0018E5EA /* ProfileProviding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ProfileProviding+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t036FFA2C2D45110C00998D8A /* ChangePasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordView.swift; sourceTree = \"<group>\"; };\n\t\t036FFA2E2D45197300998D8A /* PrivacySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsView.swift; sourceTree = \"<group>\"; };\n\t\t0370299C2D6B70F400B749DF /* MockApiClient+Realistic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"MockApiClient+Realistic.swift\"; sourceTree = \"<group>\"; };\n\t\t0370299E2D6B743B00B749DF /* PostMockType+Realistic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PostMockType+Realistic.swift\"; sourceTree = \"<group>\"; };\n\t\t037029A02D6B9A3900B749DF /* View+TabBarPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+TabBarPreview.swift\"; sourceTree = \"<group>\"; };\n\t\t037029A22D6B9B8400B749DF /* ContentView+Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ContentView+Tab.swift\"; sourceTree = \"<group>\"; };\n\t\t0372EBCD2D36FBCF00257095 /* RegistrationApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"RegistrationApplication+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0372EC1F2D370F0200257095 /* RegistrationApplicationDenialEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationApplicationDenialEditorView.swift; sourceTree = \"<group>\"; };\n\t\t037331A32C9CB12D00C826E1 /* EnvironmentValues+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"EnvironmentValues+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t037352322F27A83900341673 /* PostPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostPollView.swift; sourceTree = \"<group>\"; };\n\t\t0377BD742DE219A400E38593 /* OnboardingUsernameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingUsernameView.swift; sourceTree = \"<group>\"; };\n\t\t0377BD782DE22D4E00E38593 /* UsernameValidity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UsernameValidity+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0377BE062DE645E100E38593 /* OnboardingRecommendInstanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingRecommendInstanceView.swift; sourceTree = \"<group>\"; };\n\t\t0377BE3A2DE8E70D00E38593 /* HapticLevel+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"HapticLevel+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0377BE9A2DEA328900E38593 /* OnboardingEmailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingEmailView.swift; sourceTree = \"<group>\"; };\n\t\t0377BE9E2DEA361600E38593 /* OnboardingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingModel.swift; sourceTree = \"<group>\"; };\n\t\t037DE0742CE023E3007F7B92 /* BlockListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListView.swift; sourceTree = \"<group>\"; };\n\t\t037DE0792CE108D9007F7B92 /* FooterLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FooterLinkView.swift; sourceTree = \"<group>\"; };\n\t\t037F77EC2D3B064B00D4E180 /* SettingsDeviceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDeviceView.swift; sourceTree = \"<group>\"; };\n\t\t037F783E2D3BD05500D4E180 /* PostSettingsView+PostSizePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PostSettingsView+PostSizePicker.swift\"; sourceTree = \"<group>\"; };\n\t\t037F78402D3C00E600D4E180 /* SettingsInteractionBarSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInteractionBarSummaryView.swift; sourceTree = \"<group>\"; };\n\t\t037F78422D3C129B00D4E180 /* PostThumbnailSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostThumbnailSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t037F78442D3C25AB00D4E180 /* PostSubscriptionIndicatorSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSubscriptionIndicatorSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t037FC06F2E4A6B16009E3E63 /* InstanceView+About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InstanceView+About.swift\"; sourceTree = \"<group>\"; };\n\t\t038028D22CAB3D2D0091A8A2 /* ShareActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareActivity.swift; sourceTree = \"<group>\"; };\n\t\t038028D42CAB479D0091A8A2 /* PostEditorView+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PostEditorView+Views.swift\"; sourceTree = \"<group>\"; };\n\t\t038028D72CACAB960091A8A2 /* ModeratorSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModeratorSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t038028D92CACACD30091A8A2 /* PostEllipsisMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEllipsisMenus.swift; sourceTree = \"<group>\"; };\n\t\t038028F52CB096960091A8A2 /* SearchView+FilterModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SearchView+FilterModels.swift\"; sourceTree = \"<group>\"; };\n\t\t038028F72CB097A10091A8A2 /* SearchView+InstancePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SearchView+InstancePicker.swift\"; sourceTree = \"<group>\"; };\n\t\t038028F92CB097CB0091A8A2 /* SearchView+LocationPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SearchView+LocationPicker.swift\"; sourceTree = \"<group>\"; };\n\t\t038028FC2CB72A2A0091A8A2 /* ContentRemovalEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentRemovalEditorView.swift; sourceTree = \"<group>\"; };\n\t\t038028FE2CB72AC90091A8A2 /* ReasonShortcutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReasonShortcutView.swift; sourceTree = \"<group>\"; };\n\t\t0380965E2C10AA80003ED1D8 /* AppState+Transition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"AppState+Transition.swift\"; sourceTree = \"<group>\"; };\n\t\t038096602C10AAD8003ED1D8 /* TransitionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionView.swift; sourceTree = \"<group>\"; };\n\t\t038188982D43E0F30073E88D /* SafetySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafetySettingsView.swift; sourceTree = \"<group>\"; };\n\t\t0381889A2D4412A40073E88D /* SafetyBlurNsfwSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafetyBlurNsfwSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t0381F7132F670F95008A7731 /* SwipeActionConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionConfiguration.swift; sourceTree = \"<group>\"; };\n\t\t0381F7152F671427008A7731 /* PostBarConfiguration+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PostBarConfiguration+Types.swift\"; sourceTree = \"<group>\"; };\n\t\t0381F7232F672258008A7731 /* CommentBarConfiguration+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CommentBarConfiguration+Types.swift\"; sourceTree = \"<group>\"; };\n\t\t0381F7272F6724D3008A7731 /* ReplyBarConfiguration+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ReplyBarConfiguration+Types.swift\"; sourceTree = \"<group>\"; };\n\t\t0382A7EF2C09F0F800C79DDA /* PersonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersonView.swift; sourceTree = \"<group>\"; };\n\t\t0382A7F12C0A758E00C79DDA /* ProfileDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDateView.swift; sourceTree = \"<group>\"; };\n\t\t0382A7F32C0A76A900C79DDA /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Date+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0389DDC22C38907C0005B808 /* Message1Providing+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Message1Providing+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0389DDC42C38917A0005B808 /* InboxItemProviding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InboxItemProviding+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0389DDC62C389F840005B808 /* UnreadCount+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UnreadCount+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0389DDC82C39658E0005B808 /* Binding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Binding+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0389DDCE2C39CB0E0005B808 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = \"<group>\"; };\n\t\t0389DDD02C39E1030005B808 /* InstanceListRowBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceListRowBody.swift; sourceTree = \"<group>\"; };\n\t\t0389DDD22C39E4D40005B808 /* PasteLinkButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteLinkButtonView.swift; sourceTree = \"<group>\"; };\n\t\t0389DDD42C39F1290005B808 /* CommunityListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityListRow.swift; sourceTree = \"<group>\"; };\n\t\t0389DDDA2C3AB6340005B808 /* ActionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBuilder.swift; sourceTree = \"<group>\"; };\n\t\t038C85682D861A2100543F70 /* Comment+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Comment+Mock.swift\"; sourceTree = \"<group>\"; };\n\t\t038C85D72D87696F00543F70 /* FeedToolbarOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedToolbarOptions.swift; sourceTree = \"<group>\"; };\n\t\t038C85E52D88337100543F70 /* CommentMockType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentMockType.swift; sourceTree = \"<group>\"; };\n\t\t038C86562D888EC100543F70 /* CommentSortType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CommentSortType+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t038E1ABB2F58B9EF00D30F01 /* CommunityActionConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityActionConfiguration.swift; sourceTree = \"<group>\"; };\n\t\t038E1ABF2F58C76A00D30F01 /* SwipeActionEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionEditorView.swift; sourceTree = \"<group>\"; };\n\t\t038E1AC92F59C18100D30F01 /* CommunitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunitySettingsView.swift; sourceTree = \"<group>\"; };\n\t\t038E5C122F6D617C00C54DEB /* ImageViewerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewerSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t038E5E882F6D694500C54DEB /* ImageViewerShowControlsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewerShowControlsSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t038E5E8A2F6D6C3800C54DEB /* ImageViewerDismissSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewerDismissSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t038E62DF2F6F0FC600C54DEB /* ContextMenuConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuConfiguration.swift; sourceTree = \"<group>\"; };\n\t\t0391E0F82CFF17AE0040CCA8 /* ProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t0391E0FA2D0066240040CCA8 /* ImageUploadMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadMenu.swift; sourceTree = \"<group>\"; };\n\t\t0391E0FD2D05B2DF0040CCA8 /* AdvancedSortView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSortView.swift; sourceTree = \"<group>\"; };\n\t\t0395BCF72D9C57DE00865B33 /* View+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+Background.swift\"; sourceTree = \"<group>\"; };\n\t\t0397D45F2C66113F002C6CDC /* CommentBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBodyView.swift; sourceTree = \"<group>\"; };\n\t\t0397D4632C676CA8002C6CDC /* FeedSortPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedSortPicker.swift; sourceTree = \"<group>\"; };\n\t\t0397D46B2C67E583002C6CDC /* SortingSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortingSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t0397D4712C68078F002C6CDC /* PrefetchingConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PrefetchingConfiguration+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0397D4792C693444002C6CDC /* ReportEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportEditorView.swift; sourceTree = \"<group>\"; };\n\t\t0397D47F2C693A88002C6CDC /* [BlockNode]+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"[BlockNode]+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0397D4852C6A24D2002C6CDC /* ReportableProviding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ReportableProviding+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t0397D48B2C6BE9A2002C6CDC /* CollapsibleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleSection.swift; sourceTree = \"<group>\"; };\n\t\t0397D48F2C6CE871002C6CDC /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = \"<group>\"; };\n\t\t0397D4922C6CE87E002C6CDC /* PostEditorTargetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorTargetView.swift; sourceTree = \"<group>\"; };\n\t\t0397D4992C6EA6EE002C6CDC /* InteractionBarEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionBarEditorView.swift; sourceTree = \"<group>\"; };\n\t\t0397D49B2C6EA73C002C6CDC /* InteractionBarConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionBarConfiguration.swift; sourceTree = \"<group>\"; };\n\t\t0397D4A12C6EB035002C6CDC /* ActionAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionAppearance.swift; sourceTree = \"<group>\"; };\n\t\t0397D4A32C6FBC04002C6CDC /* ActionAppearance+StaticValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ActionAppearance+StaticValues.swift\"; sourceTree = \"<group>\"; };\n\t\t039D75632C4EEE69004F24C2 /* DeletableProviding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"DeletableProviding+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t039EFEC22BEEBEE0003AC372 /* LoginInstancePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginInstancePickerView.swift; sourceTree = \"<group>\"; };\n\t\t039F58802C7A7E5900C61658 /* JumpButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpButtonView.swift; sourceTree = \"<group>\"; };\n\t\t039F58832C7A7F2C00C61658 /* CommentJumpButtonLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentJumpButtonLocation.swift; sourceTree = \"<group>\"; };\n\t\t039F58852C7A810100C61658 /* ExpandedPostView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ExpandedPostView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t039F58872C7B531800C61658 /* SquircleLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquircleLabelStyle.swift; sourceTree = \"<group>\"; };\n\t\t039F58892C7B54FE00C61658 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t039F588B2C7B574E00C61658 /* AdvancedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t039F588E2C7B599800C61658 /* ThemeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeLabel.swift; sourceTree = \"<group>\"; };\n\t\t039F58902C7B5C7A00C61658 /* ContentView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ContentView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t039F58922C7B616600C61658 /* CommentSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t039F58942C7B618F00C61658 /* InboxSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t039F58962C7B68F100C61658 /* AboutMlemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutMlemView.swift; sourceTree = \"<group>\"; };\n\t\t039F58982C7B697D00C61658 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Bundle+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03A630EC2D497005009A47A6 /* ExternalLinkSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalLinkSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03A630EE2D497143009A47A6 /* TappableLinksSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableLinksSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03A630F02D497674009A47A6 /* ShieldsBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShieldsBadgeView.swift; sourceTree = \"<group>\"; };\n\t\t03A630F32D4976EB009A47A6 /* ShieldsBadgeView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ShieldsBadgeView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t03A6315C2D4D15F1009A47A6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text; path = PrivacyInfo.xcprivacy; sourceTree = \"<group>\"; };\n\t\t03A6315D2D4D1A1B009A47A6 /* DefaultFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultFeedSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03A6315F2D4D1CBB009A47A6 /* HapticSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03A6316C2D4E3D24009A47A6 /* ModeratorActionSeparationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModeratorActionSeparationSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03A631CA2D4FCEF7009A47A6 /* Post+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Post+Mock.swift\"; sourceTree = \"<group>\"; };\n\t\t03A814282ED1BCA90023E9E8 /* ModlogView+Filters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ModlogView+Filters.swift\"; sourceTree = \"<group>\"; };\n\t\t03A818AC2EDCDBA20023E9E8 /* FederationMode+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"FederationMode+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03A82FA02C0D1E8500D01A5C /* ApiClient+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ApiClient+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03A82FA22C0D1F2400D01A5C /* View+ExternalApiWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+ExternalApiWarning.swift\"; sourceTree = \"<group>\"; };\n\t\t03A9FD172D7CFC20007A734D /* Palette+Oled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Palette+Oled.swift\"; sourceTree = \"<group>\"; };\n\t\t03A9FD192D7CFC69007A734D /* Palette+Monochrome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Palette+Monochrome.swift\"; sourceTree = \"<group>\"; };\n\t\t03A9FD1B2D7CFD5C007A734D /* Palette+Solarized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Palette+Solarized.swift\"; sourceTree = \"<group>\"; };\n\t\t03A9FD1D2D7CFF0D007A734D /* Palette+Dracula.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Palette+Dracula.swift\"; sourceTree = \"<group>\"; };\n\t\t03A9FD1F2D7D0072007A734D /* PaletteOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteOption.swift; sourceTree = \"<group>\"; };\n\t\t03AB484E2CBAE33500567FF9 /* MarkdownWithLinkList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownWithLinkList.swift; sourceTree = \"<group>\"; };\n\t\t03AB48512CBC042E00567FF9 /* AccountContentSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountContentSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03AB48542CBC0B8000567FF9 /* AccountAdvancedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountAdvancedSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03AB48562CBC0DFC00567FF9 /* AccountSignInSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSignInSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03AB48582CBC14CE00567FF9 /* AccountEmailSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountEmailSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03AB906E2D418C200054D9E1 /* CommentJumpButtonSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentJumpButtonSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03ABE5B52DB79A0E00374AFF /* DateComponents+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"DateComponents+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03ABE5E32DB8E57000374AFF /* AccountAgeVisibilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountAgeVisibilitySettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03ACE7192DFD57CC00FD66C0 /* SiteSoftware+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SiteSoftware+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03AD09E72CF88007001EF9F7 /* MoreRepliesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreRepliesButton.swift; sourceTree = \"<group>\"; };\n\t\t03AD0A812CFDBFA0001EF9F7 /* AccountLocalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountLocalSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03AD0A832CFDC557001EF9F7 /* AccountNicknameFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountNicknameFieldView.swift; sourceTree = \"<group>\"; };\n\t\t03AF91DC2C1B23E500E56644 /* ImageViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewer.swift; sourceTree = \"<group>\"; };\n\t\t03AF91E02C1B25DE00E56644 /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UIDevice+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03AF91E22C1C616F00E56644 /* InteractionBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionBarView.swift; sourceTree = \"<group>\"; };\n\t\t03AF91E42C1C61FA00E56644 /* PostBarConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBarConfiguration.swift; sourceTree = \"<group>\"; };\n\t\t03AF91E92C1CE96600E56644 /* Counter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = \"<group>\"; };\n\t\t03AFD0DE2C3B2E000054B8AD /* PersonListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonListRow.swift; sourceTree = \"<group>\"; };\n\t\t03AFD0E02C3B30390054B8AD /* InstanceListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceListRow.swift; sourceTree = \"<group>\"; };\n\t\t03AFD0E22C3C0C540054B8AD /* InstanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceView.swift; sourceTree = \"<group>\"; };\n\t\t03B045F52E26D64900540EFB /* SiteSoftwareType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SiteSoftwareType+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03B04FBF2C5FC32300824128 /* SimpleAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleAvatarView.swift; sourceTree = \"<group>\"; };\n\t\t03B0EB6E2C87827A00F79FDF /* ExpandedPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandedPostView.swift; sourceTree = \"<group>\"; };\n\t\t03B25B2E2CC43F8600EB6DF5 /* InstanceSafetyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSafetyView.swift; sourceTree = \"<group>\"; };\n\t\t03B25B302CC4403500EB6DF5 /* Fediseer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fediseer.swift; sourceTree = \"<group>\"; };\n\t\t03B25B322CC440A600EB6DF5 /* FediseerOpinionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FediseerOpinionView.swift; sourceTree = \"<group>\"; };\n\t\t03B25B342CC4446400EB6DF5 /* FediseerOpinionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FediseerOpinionListView.swift; sourceTree = \"<group>\"; };\n\t\t03B25B362CC4478600EB6DF5 /* FediseerInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FediseerInfoView.swift; sourceTree = \"<group>\"; };\n\t\t03B25B3A2CC44FFF00EB6DF5 /* UploadConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadConfirmationView.swift; sourceTree = \"<group>\"; };\n\t\t03B431B12C44409D001A1EB5 /* CommentEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentEditorView.swift; sourceTree = \"<group>\"; };\n\t\t03B431B32C4481C3001A1EB5 /* MarkdownTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownTextEditor.swift; sourceTree = \"<group>\"; };\n\t\t03B431B52C454D49001A1EB5 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UIImage+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03B431BB2C455838001A1EB5 /* LargePostBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargePostBodyView.swift; sourceTree = \"<group>\"; };\n\t\t03B431BF2C45ABFB001A1EB5 /* UITextView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UITextView+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03B431C12C45BA00001A1EB5 /* MarkdownEditorToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownEditorToolbarView.swift; sourceTree = \"<group>\"; };\n\t\t03B431C32C45BA45001A1EB5 /* AccountPickerMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPickerMenu.swift; sourceTree = \"<group>\"; };\n\t\t03B62B762CE295530077E9C8 /* RulesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RulesListView.swift; sourceTree = \"<group>\"; };\n\t\t03B62B782CE2A2C00077E9C8 /* RulesPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RulesPickerView.swift; sourceTree = \"<group>\"; };\n\t\t03B62C3F2CE811CB0077E9C8 /* PersonBanEditorView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PersonBanEditorView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\t03B72B662C2888EE0023A6C4 /* View+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+ContextMenu.swift\"; sourceTree = \"<group>\"; };\n\t\t03B72B6A2C28A0190023A6C4 /* SubscriptionListSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionListSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03B7F3342EEEC70F00B00F6A /* NoteEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteEditorView.swift; sourceTree = \"<group>\"; };\n\t\t03BF11C22D3D135D00CC1F66 /* SearchView+CreatorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SearchView+CreatorPicker.swift\"; sourceTree = \"<group>\"; };\n\t\t03BF11C42D3D3AFB00CC1F66 /* PostReadIndicatorSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostReadIndicatorSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03BF11C62D3D634A00CC1F66 /* AccessibilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilitySettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03BF11C92D4027E900CC1F66 /* DevicePickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicePickerItem.swift; sourceTree = \"<group>\"; };\n\t\t03BF11CB2D404F2C00CC1F66 /* CommentMaximumDepthSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentMaximumDepthSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03C93CEF2BEFFB1A00327BFE /* LoginCredentialsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCredentialsView.swift; sourceTree = \"<group>\"; };\n\t\t03CBD18C2C6120F600E870BC /* PersonFlair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonFlair.swift; sourceTree = \"<group>\"; };\n\t\t03CCDA9F2BF2795300C0C851 /* LoginPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginPage.swift; sourceTree = \"<group>\"; };\n\t\t03CCDAA32BF2852E00C0C851 /* LoginTotpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginTotpView.swift; sourceTree = \"<group>\"; };\n\t\t03D001DC2EC53A97001BF97D /* NavigationPage+PresentationDetents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"NavigationPage+PresentationDetents.swift\"; sourceTree = \"<group>\"; };\n\t\t03D006312ECCBA95001BF97D /* QuickSwipeAction+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"QuickSwipeAction+Actions.swift\"; sourceTree = \"<group>\"; };\n\t\t03D0273B2CD3BA5100984519 /* PersonContent+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PersonContent+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03D283F92D256E1E00A6659B /* VisitHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitHistory.swift; sourceTree = \"<group>\"; };\n\t\t03D283FB2D25A3F700A6659B /* VisitHistory+CodedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"VisitHistory+CodedData.swift\"; sourceTree = \"<group>\"; };\n\t\t03D283FD2D25EEC500A6659B /* SearchView+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SearchView+Views.swift\"; sourceTree = \"<group>\"; };\n\t\t03D283FF2D26F09500A6659B /* Instance+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Instance+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03D284012D29E03C00A6659B /* FeedFilterButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedFilterButtonStyle.swift; sourceTree = \"<group>\"; };\n\t\t03D284052D2AEE3A00A6659B /* TabBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03D2A6362C00F92400ED4FF2 /* Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = \"<group>\"; };\n\t\t03D2A6382C00FAE000ED4FF2 /* UserAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAccount.swift; sourceTree = \"<group>\"; };\n\t\t03D2A63A2C010B7500ED4FF2 /* GuestAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestAccount.swift; sourceTree = \"<group>\"; };\n\t\t03D2A63C2C010CD400ED4FF2 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = \"<group>\"; };\n\t\t03D2A63E2C010DBF00ED4FF2 /* GuestSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestSession.swift; sourceTree = \"<group>\"; };\n\t\t03D2A6412C011F4A00ED4FF2 /* AccountListRowBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListRowBody.swift; sourceTree = \"<group>\"; };\n\t\t03D3A1D32BB88EF1009DE55E /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = \"<group>\"; };\n\t\t03D3A1E42BB8B7A3009DE55E /* ActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionType.swift; sourceTree = \"<group>\"; };\n\t\t03D3A1EE2BB9CA1D009DE55E /* MenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuButton.swift; sourceTree = \"<group>\"; };\n\t\t03D3A1F02BB9D48E009DE55E /* BasicAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAction.swift; sourceTree = \"<group>\"; };\n\t\t03D3A1F22BB9D49B009DE55E /* ActionGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionGroup.swift; sourceTree = \"<group>\"; };\n\t\t03D65D6F2F4B046F0041ADAF /* ContextMenuSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03D662A42F5377630041ADAF /* ActionSeedSections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionSeedSections.swift; sourceTree = \"<group>\"; };\n\t\t03DAEA762C64074E0064DE64 /* SubscriptionListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionListItemView.swift; sourceTree = \"<group>\"; };\n\t\t03DD69412D4FDE8900F8950D /* Person+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Person+Mock.swift\"; sourceTree = \"<group>\"; };\n\t\t03DD69432D4FEA0000F8950D /* PreviewModifier+SampleEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PreviewModifier+SampleEnvironment.swift\"; sourceTree = \"<group>\"; };\n\t\t03E0EF422CA73D7A002CB66C /* PostStubResolutionPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostStubResolutionPage.swift; sourceTree = \"<group>\"; };\n\t\t03E0EF442CA74036002CB66C /* CommentPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentPage.swift; sourceTree = \"<group>\"; };\n\t\t03E46ACA2D1216CE002589DB /* PostViewLinkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewLinkType.swift; sourceTree = \"<group>\"; };\n\t\t03E46ACC2D121C19002589DB /* HeadlinePostBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinePostBodyView.swift; sourceTree = \"<group>\"; };\n\t\t03E46AD12D130681002589DB /* VotesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotesListView.swift; sourceTree = \"<group>\"; };\n\t\t03E46AD32D130728002589DB /* ScoringOperation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ScoringOperation+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\t03E614E62C0BCDC200F692A4 /* FullyQualifiedLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullyQualifiedLinkView.swift; sourceTree = \"<group>\"; };\n\t\t03EC83242E916C51004698BB /* View+SafeAreaBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+SafeAreaBar.swift\"; sourceTree = \"<group>\"; };\n\t\t03EC83EF2E9590D3004698BB /* LinkHostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkHostView.swift; sourceTree = \"<group>\"; };\n\t\t03EC84412E959AA5004698BB /* PostEditorWebsitePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorWebsitePreviewView.swift; sourceTree = \"<group>\"; };\n\t\t03EC86452E9E9D4C004698BB /* PopupAnchorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupAnchorModel.swift; sourceTree = \"<group>\"; };\n\t\t03ECD7182C81195000D48BF6 /* PostEditorView+LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PostEditorView+LinkView.swift\"; sourceTree = \"<group>\"; };\n\t\t03ECD71A2C811D6700D48BF6 /* PostEditorView+ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PostEditorView+ImageView.swift\"; sourceTree = \"<group>\"; };\n\t\t03ECD71E2C864DB700D48BF6 /* ImageUploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadManager.swift; sourceTree = \"<group>\"; };\n\t\t03ECD7202C8654BA00D48BF6 /* PostEditorView+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PostEditorView+Toolbar.swift\"; sourceTree = \"<group>\"; };\n\t\t03F6BD932D500DED006A425E /* PersonMockType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonMockType.swift; sourceTree = \"<group>\"; };\n\t\t03F6BD972D500E2A006A425E /* PersonMockType+Realistic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PersonMockType+Realistic.swift\"; sourceTree = \"<group>\"; };\n\t\t03F6BD992D501041006A425E /* SeededRandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeededRandomNumberGenerator.swift; sourceTree = \"<group>\"; };\n\t\t03F6BD9B2D501478006A425E /* ActorIdentifier+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ActorIdentifier+Mock.swift\"; sourceTree = \"<group>\"; };\n\t\t03F6BDAC2D516615006A425E /* Community+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Community+Mock.swift\"; sourceTree = \"<group>\"; };\n\t\t03F6BDAE2D516636006A425E /* CommunityMockType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityMockType.swift; sourceTree = \"<group>\"; };\n\t\t03F6BDB02D52AA00006A425E /* CommunityMockType+Realistic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CommunityMockType+Realistic.swift\"; sourceTree = \"<group>\"; };\n\t\t03F6BDBB2D52B7FE006A425E /* PostMockType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostMockType.swift; sourceTree = \"<group>\"; };\n\t\t03F6BDF72D555F6E006A425E /* ModMailInteractionBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModMailInteractionBarSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t03F967262CE218110081C9A3 /* PersonBanEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonBanEditorView.swift; sourceTree = \"<group>\"; };\n\t\t03F9672A2CE221220081C9A3 /* Label+Profile1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Label+Profile1.swift\"; sourceTree = \"<group>\"; };\n\t\t03FA318E2C6FEF2000D47FA3 /* InteractionBarActionLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionBarActionLabelView.swift; sourceTree = \"<group>\"; };\n\t\t03FD6CAF2C9B719100500FD6 /* View+PopupAnchor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+PopupAnchor.swift\"; sourceTree = \"<group>\"; };\n\t\t03FE14032BF93FDD00A8377F /* ErrorDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetails.swift; sourceTree = \"<group>\"; };\n\t\t03FE14072BF94FFB00A8377F /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = \"<group>\"; };\n\t\t03FE140B2BF953B000A8377F /* HandleError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleError.swift; sourceTree = \"<group>\"; };\n\t\t630D753C27F65E44006E60C9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t6332FDBC27EFAF7B0009A98A /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = \"wrapper.plug-in\"; path = Settings.bundle; sourceTree = \"<group>\"; };\n\t\t6363D5C127EE196700E34822 /* Mlem.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mlem.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t6363D5C427EE196700E34822 /* MlemApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemApp.swift; sourceTree = \"<group>\"; };\n\t\t6363D5C627EE196700E34822 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = \"<group>\"; };\n\t\t6363D5C827EE196A00E34822 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t6363D5D627EE196A00E34822 /* MlemTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MlemTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t6363D5E027EE196A00E34822 /* MlemUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MlemUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t6DE1183B2A4A217400810C7E /* Profile View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Profile View.swift\"; sourceTree = \"<group>\"; };\n\t\t814CEF622F44577A0090F812 /* HiddenReadBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenReadBannerView.swift; sourceTree = \"<group>\"; };\n\t\t81A179BB2DDE591700B17017 /* LongPressActionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressActionSettingsView.swift; sourceTree = \"<group>\"; };\n\t\t81A179BE2DDE5BF300B17017 /* TabBarLongPressAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarLongPressAction.swift; sourceTree = \"<group>\"; };\n\t\t81C4B4322F493C5E001406A1 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = \"<group>\"; };\n\t\t81DE61C32F48AF44006E4C36 /* OpenInMlem.appex */ = {isa = PBXFileReference; explicitFileType = \"wrapper.app-extension\"; includeInIndex = 0; path = OpenInMlem.appex; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t81DE61C42F48AF44006E4C36 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };\n\t\t81DE61D62F48AF4C006E4C36 /* Action.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Action.js; sourceTree = \"<group>\"; };\n\t\t81DE61D72F48AF4C006E4C36 /* ActionRequestHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionRequestHandler.swift; sourceTree = \"<group>\"; };\n\t\t81DE61D82F48AF4C006E4C36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t81DE61D92F48AF4C006E4C36 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = \"<group>\"; };\n\t\tAD1B0D362A5F7A260006F554 /* Licenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Licenses.swift; sourceTree = \"<group>\"; };\n\t\tB104A6E12A5AFC9F00B3E725 /* Mlem.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mlem.entitlements; sourceTree = \"<group>\"; };\n\t\tB1B78D632A51D53900F72485 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\tCD0374292D1DBFCF001E85FA /* ReadCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadCheck.swift; sourceTree = \"<group>\"; };\n\t\tCD03B5BD2F3BA16100AEF786 /* Blockable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Blockable+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCD0C5C0F2E9962970074D5A4 /* ExportablePostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportablePostEditorView.swift; sourceTree = \"<group>\"; };\n\t\tCD0E06F62C0E739F00445849 /* PostType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PostType+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCD0E07002C12707700445849 /* ToolbarEllipsisMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarEllipsisMenu.swift; sourceTree = \"<group>\"; };\n\t\tCD0F28092C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+IsAtTopSubscriber.swift\"; sourceTree = \"<group>\"; };\n\t\tCD10FA762C7A8622008985AD /* ImageSaver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSaver.swift; sourceTree = \"<group>\"; };\n\t\tCD13CC582C583C7A001AF428 /* WebsitePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsitePreviewView.swift; sourceTree = \"<group>\"; };\n\t\tCD13CC5A2C588B34001AF428 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = \"<group>\"; };\n\t\tCD13CC642C5D2B9D001AF428 /* CircleCroppedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleCroppedImageView.swift; sourceTree = \"<group>\"; };\n\t\tCD1446202A5B328E00610EF1 /* Privacy Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Privacy Policy.swift\"; sourceTree = \"<group>\"; };\n\t\tCD1446242A5B357900610EF1 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = \"<group>\"; };\n\t\tCD1446262A5B36DA00610EF1 /* EULA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EULA.swift; sourceTree = \"<group>\"; };\n\t\tCD1B2E202C7F84160075C7EA /* View+MarkReadOnScroll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+MarkReadOnScroll.swift\"; sourceTree = \"<group>\"; };\n\t\tCD1C64142D34286E0006B3C1 /* CommunityView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CommunityView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\tCD1D31822C56D742001B434B /* View+WidthReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+WidthReader.swift\"; sourceTree = \"<group>\"; };\n\t\tCD1DF6372D38357100F7851E /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = \"<group>\"; };\n\t\tCD1DF63D2D387E8300F7851E /* MediaView+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"MediaView+Views.swift\"; sourceTree = \"<group>\"; };\n\t\tCD24CAFB2D5568F90032B5E8 /* DiscussionLanguageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionLanguageSettingsView.swift; sourceTree = \"<group>\"; };\n\t\tCD2783992D9B365D00DD4C69 /* ZoomableImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableImageView.swift; sourceTree = \"<group>\"; };\n\t\tCD2C86532D5556BE0034CD8A /* MlemError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemError.swift; sourceTree = \"<group>\"; };\n\t\tCD3153062C38421B00BC5FBE /* View+LoadFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+LoadFeed.swift\"; sourceTree = \"<group>\"; };\n\t\tCD332D782CA7175200A53988 /* PlayButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayButton.swift; sourceTree = \"<group>\"; };\n\t\tCD332D7D2CA7485D00A53988 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"String+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCD33CA512D3C18AE00106C8C /* ImageViewer+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ImageViewer+Views.swift\"; sourceTree = \"<group>\"; };\n\t\tCD3485BA2D501463006748B8 /* ZoomSliderLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomSliderLocation.swift; sourceTree = \"<group>\"; };\n\t\tCD3485BC2D50156E006748B8 /* ZoomSliderSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomSliderSettingsView.swift; sourceTree = \"<group>\"; };\n\t\tCD3FC67F2D4A75050088E63B /* CounterApperance+StaticValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CounterApperance+StaticValues.swift\"; sourceTree = \"<group>\"; };\n\t\tCD43E8B22BF2C24E007C3D71 /* ContentLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLoader.swift; sourceTree = \"<group>\"; };\n\t\tCD44C92B2E5CC8B600F24AC8 /* ConditionalLabelStyleViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalLabelStyleViewModifier.swift; sourceTree = \"<group>\"; };\n\t\tCD45CB0C2D1880E3008BC729 /* FiltersSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersSettingsView.swift; sourceTree = \"<group>\"; };\n\t\tCD4B66D92DCE809A00D28EB4 /* InstanceUptimeView+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InstanceUptimeView+Views.swift\"; sourceTree = \"<group>\"; };\n\t\tCD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedsView.swift; sourceTree = \"<group>\"; };\n\t\tCD4D583E2B86855F00B82964 /* MlemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemTests.swift; sourceTree = \"<group>\"; };\n\t\tCD4D58402B86858100B82964 /* MlemUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemUITests.swift; sourceTree = \"<group>\"; };\n\t\tCD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceRepository.swift; sourceTree = \"<group>\"; };\n\t\tCD4D58A92B86BE5900B82964 /* AccountListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListView.swift; sourceTree = \"<group>\"; };\n\t\tCD4D58AC2B86BE7100B82964 /* QuickSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSwitcherView.swift; sourceTree = \"<group>\"; };\n\t\tCD4D58B22B86BFD400B82964 /* AccountsTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsTracker.swift; sourceTree = \"<group>\"; };\n\t\tCD4D58B42B86BFFA00B82964 /* PersistenceRepository+Dependency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = \"PersistenceRepository+Dependency.swift\"; sourceTree = \"<group>\"; };\n\t\tCD4D58B82B86D9F800B82964 /* AccountListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListRow.swift; sourceTree = \"<group>\"; };\n\t\tCD4D58BA2B86DA7D00B82964 /* AccountListView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"AccountListView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\tCD4D58C72B86DCED00B82964 /* AvatarType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarType.swift; sourceTree = \"<group>\"; };\n\t\tCD4D58CE2B86DDEC00B82964 /* AccountSortMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AccountSortMode.swift; path = Mlem/App/Protocols/AccountSortMode.swift; sourceTree = SOURCE_ROOT; };\n\t\tCD4D58EA2B86E63300B82964 /* AssociatedColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociatedColor.swift; sourceTree = \"<group>\"; };\n\t\tCD4D58F72B87B0D100B82964 /* InternetSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetSpeed.swift; sourceTree = \"<group>\"; };\n\t\tCD4D59132B87B36B00B82964 /* InternetConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetConnectionManager.swift; sourceTree = \"<group>\"; };\n\t\tCD4D59152B87B38C00B82964 /* UIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UIApplication+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCD4D59172B87B3B000B82964 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UIViewController+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCD4E386C2C836F8C009B24F2 /* UIUserInterfaceStyle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UIUserInterfaceStyle+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabReselectTracker.swift; sourceTree = \"<group>\"; };\n\t\tCD4ED8492BF1113800EFA0A2 /* View+TabReselectConsumer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+TabReselectConsumer.swift\"; sourceTree = \"<group>\"; };\n\t\tCD5581DD2C7B8B820043FAC3 /* ImageFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFunctions.swift; sourceTree = \"<group>\"; };\n\t\tCD57AFA42D0377E400AB3956 /* AnimationControlLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationControlLayer.swift; sourceTree = \"<group>\"; };\n\t\tCD5C197C2D97096D0089614C /* ZoomCurves.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomCurves.swift; sourceTree = \"<group>\"; };\n\t\tCD5C197E2D970E570089614C /* MomentumStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MomentumStatus.swift; sourceTree = \"<group>\"; };\n\t\tCD5CAA0B2D41AE52008E20F2 /* EmbeddingSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddingSettingsView.swift; sourceTree = \"<group>\"; };\n\t\tCD5D8E2A2D6D5AAE00AF5CE4 /* CGSize+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CGSize+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCD635E1A2C94DACD00864F75 /* BypassProxyWarningSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BypassProxyWarningSheet.swift; sourceTree = \"<group>\"; };\n\t\tCD64364F2D483C8F002668FB /* InteractionBarEditorView+Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InteractionBarEditorView+Views.swift\"; sourceTree = \"<group>\"; };\n\t\tCD6DC0292D86513800693B16 /* AnimatedAvatarBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedAvatarBehavior.swift; sourceTree = \"<group>\"; };\n\t\tCD6DC02B2D86540500693B16 /* AnimatedAvatarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedAvatarSettingsView.swift; sourceTree = \"<group>\"; };\n\t\tCD737FB92F771BF100E46411 /* InstanceSummarySoftware+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InstanceSummarySoftware+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCD756A952ED765E90031D7D1 /* ExportableCommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportableCommentView.swift; sourceTree = \"<group>\"; };\n\t\tCD756A972ED766930031D7D1 /* ExportableCommentEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportableCommentEditorView.swift; sourceTree = \"<group>\"; };\n\t\tCD77437E2C1BA5CE0085BB43 /* MultiplatformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiplatformView.swift; sourceTree = \"<group>\"; };\n\t\tCD7743882C20EDEE0085BB43 /* VotesModel+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"VotesModel+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCD7882A62BFFD1A3002E1A30 /* ThumbnailLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailLocation.swift; sourceTree = \"<group>\"; };\n\t\tCD7882A82BFFDFC7002E1A30 /* PostTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTag.swift; sourceTree = \"<group>\"; };\n\t\tCD7882AA2C013005002E1A30 /* EllipsisMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EllipsisMenu.swift; sourceTree = \"<group>\"; };\n\t\tCD79281E2C73B52A00FA712D /* EndOfFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndOfFeedView.swift; sourceTree = \"<group>\"; };\n\t\tCD7928222C73CBA400FA712D /* TileScoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileScoreView.swift; sourceTree = \"<group>\"; };\n\t\tCD7928252C73E73400FA712D /* PersonView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"PersonView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\tCD7AC2CB2D7FDFF700A671B7 /* MediaView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"MediaView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\tCD7BF9312D18F4EB0020F2C5 /* FiltersTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersTracker.swift; sourceTree = \"<group>\"; };\n\t\tCD7C4E562F3B92A600ADCBDD /* View+ReloadOnAccountSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+ReloadOnAccountSwitch.swift\"; sourceTree = \"<group>\"; };\n\t\tCD7DB9702C49C17200DCC542 /* PersonContentGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonContentGridView.swift; sourceTree = \"<group>\"; };\n\t\tCD7DB9722C4AEDDE00DCC542 /* TileCommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileCommentView.swift; sourceTree = \"<group>\"; };\n\t\tCD7DB9752C4D6C0A00DCC542 /* FeedCommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCommentView.swift; sourceTree = \"<group>\"; };\n\t\tCD869FCB2C15F8AC00FC8B5B /* BubblePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubblePickerView.swift; sourceTree = \"<group>\"; };\n\t\tCD869FCD2C15F90C00FC8B5B /* ChildSizeReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChildSizeReader.swift; sourceTree = \"<group>\"; };\n\t\tCD869FCF2C15F92E00FC8B5B /* Int+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Int+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCD87BEC22D5132BB0099F190 /* FilterViolationWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterViolationWarning.swift; sourceTree = \"<group>\"; };\n\t\tCD8DB9392D22003D00EB0C7B /* Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animations.swift; sourceTree = \"<group>\"; };\n\t\tCD93420A2DCD069800945333 /* InstanceUptimeView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"InstanceUptimeView+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\tCD950E042F0ED6F7002A0595 /* FeedPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPostView.swift; sourceTree = \"<group>\"; };\n\t\tCD9857A32C5E7F9D0084C71F /* NsfwOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NsfwOverlayView.swift; sourceTree = \"<group>\"; };\n\t\tCD9BD5802D8F8F750006AB7F /* ZoomRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomRecognizer.swift; sourceTree = \"<group>\"; };\n\t\tCD9CFA292C2E1E8400739BBC /* FeedHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedHeaderView.swift; sourceTree = \"<group>\"; };\n\t\tCD9CFA2B2C2E1EF300739BBC /* FeedIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIconView.swift; sourceTree = \"<group>\"; };\n\t\tCD9CFA2F2C2E22F600739BBC /* FeedDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedDescription.swift; sourceTree = \"<group>\"; };\n\t\tCD9CFA362C306DAB00739BBC /* PostGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostGridView.swift; sourceTree = \"<group>\"; };\n\t\tCD9D243C2CC1DF55006E5F3F /* AccountType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountType.swift; sourceTree = \"<group>\"; };\n\t\tCDA1D2A42ED8C4670077A9EA /* ExportableViewComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportableViewComponents.swift; sourceTree = \"<group>\"; };\n\t\tCDA67A9E2D9B329D00E5D17B /* CGPoint+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CGPoint+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCDA683F72C77E577000C4486 /* NsfwBlurBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NsfwBlurBehavior.swift; sourceTree = \"<group>\"; };\n\t\tCDAA02DA2C810DB200D75633 /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Calendar+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCDAA02E02C817AAB00D75633 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Color+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCDAA02E22C821C9100D75633 /* Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Divider.swift; sourceTree = \"<group>\"; };\n\t\tCDADDB262F49210A00A4214A /* CommunityStubResolutionPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityStubResolutionPage.swift; sourceTree = \"<group>\"; };\n\t\tCDB2EC7A2BFADA8B00DBC0EF /* PostSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSize.swift; sourceTree = \"<group>\"; };\n\t\tCDB2EC7C2BFADAB300DBC0EF /* CompactPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactPostView.swift; sourceTree = \"<group>\"; };\n\t\tCDB2EC7E2BFADACC00DBC0EF /* HeadlinePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinePostView.swift; sourceTree = \"<group>\"; };\n\t\tCDB2EC802BFADADF00DBC0EF /* LargePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargePostView.swift; sourceTree = \"<group>\"; };\n\t\tCDB2EC852BFADD7C00DBC0EF /* FullyQualifiedLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullyQualifiedLabelView.swift; sourceTree = \"<group>\"; };\n\t\tCDB2EC872BFAE14800DBC0EF /* FullyQualifiedNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullyQualifiedNameView.swift; sourceTree = \"<group>\"; };\n\t\tCDB2EC892BFAEFDF00DBC0EF /* ThumbnailImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailImageView.swift; sourceTree = \"<group>\"; };\n\t\tCDB3DDFC2DA485D000F407AB /* SettingsValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsValues.swift; sourceTree = \"<group>\"; };\n\t\tCDB41E892C83C24400BD2DE9 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = \"<group>\"; };\n\t\tCDB738282CB8A69D005B11BB /* View+PaletteBorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+PaletteBorder.swift\"; sourceTree = \"<group>\"; };\n\t\tCDB95FCC2EBD3212008669D9 /* SmallOverlayButtonLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallOverlayButtonLabel.swift; sourceTree = \"<group>\"; };\n\t\tCDBE78C12F38DD89008B254C /* PersonStubResolutionPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonStubResolutionPage.swift; sourceTree = \"<group>\"; };\n\t\tCDBEC30E2E84C9F300F30B00 /* ExportablePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportablePostView.swift; sourceTree = \"<group>\"; };\n\t\tCDBFCB642C03920C008CD468 /* PostLinkHostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostLinkHostView.swift; sourceTree = \"<group>\"; };\n\t\tCDBFCB692C04EFFE008CD468 /* PostSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettingsView.swift; sourceTree = \"<group>\"; };\n\t\tCDBFCB6B2C054AA7008CD468 /* TilePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilePostView.swift; sourceTree = \"<group>\"; };\n\t\tCDC44A352D1CBC200030F01C /* ReadPostIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadPostIndicator.swift; sourceTree = \"<group>\"; };\n\t\tCDCA44B32C176A4700C092B3 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Array+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCDCC1BA62D99BDB0006579DF /* GestureRecognizers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureRecognizers.swift; sourceTree = \"<group>\"; };\n\t\tCDCC1BA82D99BEA8006579DF /* ZoomRecognizerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomRecognizerCoordinator.swift; sourceTree = \"<group>\"; };\n\t\tCDCC7FEC2D9AEDBE00CE18DA /* BridgeDragValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDragValue.swift; sourceTree = \"<group>\"; };\n\t\tCDCC7FEE2D9B1E6E00CE18DA /* CachedComputation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedComputation.swift; sourceTree = \"<group>\"; };\n\t\tCDCC7FF02D9B27CE00CE18DA /* ZoomRecognizerCoordinator+GestureRecognition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ZoomRecognizerCoordinator+GestureRecognition.swift\"; sourceTree = \"<group>\"; };\n\t\tCDCC7FF22D9B283400CE18DA /* ZoomRecognizerCoordinator+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"ZoomRecognizerCoordinator+Logic.swift\"; sourceTree = \"<group>\"; };\n\t\tCDCF5A572F1426A0006748E8 /* CommentStubResolutionPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentStubResolutionPage.swift; sourceTree = \"<group>\"; };\n\t\tCDD4A09B2C8A122F0001AD1A /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = \"<group>\"; };\n\t\tCDD4A09D2C8B69FC0001AD1A /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = \"<group>\"; };\n\t\tCDD4A09F2C8B985D0001AD1A /* ImportExportSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExportSettingsView.swift; sourceTree = \"<group>\"; };\n\t\tCDD8B94B2C8234BC00510EBB /* Form.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Form.swift; sourceTree = \"<group>\"; };\n\t\tCDD8E30C2EEA07EE00FC4C8D /* ExportableCommentLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportableCommentLoader.swift; sourceTree = \"<group>\"; };\n\t\tCDD99C3B2C73F3FF0010367F /* DeleteAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountView.swift; sourceTree = \"<group>\"; };\n\t\tCDD99C3D2C73F4380010367F /* WarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WarningView.swift; sourceTree = \"<group>\"; };\n\t\tCDDA49A72F7044CD004A5AFF /* InstanceStubResolutionPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceStubResolutionPage.swift; sourceTree = \"<group>\"; };\n\t\tCDE1F18E2C63D75A008AF042 /* LegacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySettings.swift; sourceTree = \"<group>\"; };\n\t\tCDE1F1932C63DF44008AF042 /* PlatformConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformConstants.swift; sourceTree = \"<group>\"; };\n\t\tCDE1F1952C63DF89008AF042 /* PhoneConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneConstants.swift; sourceTree = \"<group>\"; };\n\t\tCDE1F1972C63DFC9008AF042 /* PadConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadConstants.swift; sourceTree = \"<group>\"; };\n\t\tCDE1F19B2C63E2EB008AF042 /* SettingPropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingPropertyWrapper.swift; sourceTree = \"<group>\"; };\n\t\tCDE1F19D2C63E306008AF042 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = \"<group>\"; };\n\t\tCDE88C342E68938800183AE5 /* View+AccountSwitcherGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+AccountSwitcherGesture.swift\"; sourceTree = \"<group>\"; };\n\t\tCDEE15512D22190000EB9D7B /* ErrorsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorsTracker.swift; sourceTree = \"<group>\"; };\n\t\tCDEE15532D22364500EB9D7B /* ErrorLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogView.swift; sourceTree = \"<group>\"; };\n\t\tCDF60A002E998BB2005FA3F1 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Data+Extensions.swift\"; sourceTree = \"<group>\"; };\n\t\tCDF8C1902D5D501E00295CBA /* InteractionBarWidgetPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionBarWidgetPickerView.swift; sourceTree = \"<group>\"; };\n\t\tCDF9EF322AB2845C003F885B /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = \"<group>\"; };\n\t\tCDFB8C682C7796020070845F /* View+DynamicBlur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"View+DynamicBlur.swift\"; sourceTree = \"<group>\"; };\n\t\tCDFF9A322D88C3BE009E02E2 /* CGFloat+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"CGFloat+Extensions.swift\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFileSystemSynchronizedRootGroup section */\n\t\t031DBA6A2F9A8D6500B4BAE4 /* Events */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Events; sourceTree = \"<group>\"; };\n\t\t03DA26AC2D79F29700E66267 /* Packages */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Packages; sourceTree = \"<group>\"; };\n\t\t03EC85ED2E9D925B004698BB /* Actions */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Actions; sourceTree = \"<group>\"; };\n\t\t03F6BDA12D502382006A425E /* Preview Content */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = \"Preview Content\"; sourceTree = \"<group>\"; };\n\t\tCD2276D42F369C5F0024AEB1 /* Person */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Person; sourceTree = \"<group>\"; };\n\t\tCD6CD11A2F25BF1300566122 /* Interactable */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Interactable; sourceTree = \"<group>\"; };\n\t\tCD6CD1212F25C36800566122 /* Post */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Post; sourceTree = \"<group>\"; };\n\t\tCD6CD1222F25EC6A00566122 /* Comment */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Comment; sourceTree = \"<group>\"; };\n\t\tCDADDB222F491D2F00A4214A /* Community */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Community; sourceTree = \"<group>\"; };\n\t\tCDC71F242F15A6B900D314B1 /* ExpectedViews */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ExpectedViews; sourceTree = \"<group>\"; };\n/* End PBXFileSystemSynchronizedRootGroup section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t6363D5BE27EE196700E34822 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0377BF9B2DF0E0E000E38593 /* Rest in Frameworks */,\n\t\t\t\t03A9FD162D7CEC09007A734D /* Theming in Frameworks */,\n\t\t\t\t038100512F6AE867008A7731 /* MlemBackend in Frameworks */,\n\t\t\t\t03FA318C2C6FECAE00D47FA3 /* Flow in Frameworks */,\n\t\t\t\t030FF6792BC84F7E00F6BFAC /* SwiftUIIntrospect in Frameworks */,\n\t\t\t\t0341480D2D8F63A6005503AF /* MlemMiddleware in Frameworks */,\n\t\t\t\tB104A6DA2A59BF3C00B3E725 /* NukeExtensions in Frameworks */,\n\t\t\t\t03EC85EC2E9D8F37004698BB /* Actions in Frameworks */,\n\t\t\t\t037386472BDAFE81007492B5 /* LemmyMarkdownUI in Frameworks */,\n\t\t\t\tCDA711FB2DB5CAC3008BC3ED /* Media in Frameworks */,\n\t\t\t\tB104A6D82A59BF3C00B3E725 /* Nuke in Frameworks */,\n\t\t\t\t0347A6FB2F97F4CF00EFD670 /* FediverseEvents in Frameworks */,\n\t\t\t\t03EC83EE2E958A44004698BB /* OpenGraph in Frameworks */,\n\t\t\t\t50C99B562A61D792005D57DD /* Dependencies in Frameworks */,\n\t\t\t\t636250DC2A18111400FC59B4 /* KeychainAccess in Frameworks */,\n\t\t\t\tCD4368C12AE23FD400BD8BD1 /* Semaphore in Frameworks */,\n\t\t\t\tB104A6DE2A59BF3C00B3E725 /* NukeVideo in Frameworks */,\n\t\t\t\t0368799A2DA1320000E796EF /* ComponentViews in Frameworks */,\n\t\t\t\t031CA5772E5900E900CF0C0F /* QuickSwipes in Frameworks */,\n\t\t\t\t0377BE292DE7A2DE00E38593 /* Haptics in Frameworks */,\n\t\t\t\t03D8BF432DA55B6900506687 /* Icons in Frameworks */,\n\t\t\t\tCDE4AC472CA372B600981010 /* SDWebImageWebPCoder in Frameworks */,\n\t\t\t\tB104A6DC2A59BF3C00B3E725 /* NukeUI in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t6363D5D327EE196A00E34822 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t6363D5DD27EE196A00E34822 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t81DE61C02F48AF44006E4C36 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t81DE61C52F48AF44006E4C36 /* UniformTypeIdentifiers.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t03049A182C6502DB00FF6889 /* Form */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0397D48B2C6BE9A2002C6CDC /* CollapsibleSection.swift */,\n\t\t\t\t03049A192C6502F300FF6889 /* FormSection.swift */,\n\t\t\t\t03049A1F2C650A8100FF6889 /* FormReadout.swift */,\n\t\t\t\t03049A1B2C65039400FF6889 /* ActiveUserCountView.swift */,\n\t\t\t);\n\t\t\tpath = Form;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t030BCB192C3EA5E20037680F /* Instance */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDDA49A72F7044CD004A5AFF /* InstanceStubResolutionPage.swift */,\n\t\t\t\tCD4B66D92DCE809A00D28EB4 /* InstanceUptimeView+Views.swift */,\n\t\t\t\tCD93420A2DCD069800945333 /* InstanceUptimeView+Logic.swift */,\n\t\t\t\t03AFD0E22C3C0C540054B8AD /* InstanceView.swift */,\n\t\t\t\t034A85702EC0A1FA00E5F904 /* InstanceCommunityListView.swift */,\n\t\t\t\t037FC06F2E4A6B16009E3E63 /* InstanceView+About.swift */,\n\t\t\t\t035394982CA1B20B00795AA5 /* InstanceView+Logic.swift */,\n\t\t\t\t030BCB1A2C3EA5FD0037680F /* InstanceDetailsView.swift */,\n\t\t\t\t035394922CA1AE2C00795AA5 /* UptimeData.swift */,\n\t\t\t\t035394942CA1AE6300795AA5 /* InstanceUptimeView.swift */,\n\t\t\t\t03B25B2E2CC43F8600EB6DF5 /* InstanceSafetyView.swift */,\n\t\t\t\t03B25B342CC4446400EB6DF5 /* FediseerOpinionListView.swift */,\n\t\t\t\t03B25B362CC4478600EB6DF5 /* FediseerInfoView.swift */,\n\t\t\t\t03B25B322CC440A600EB6DF5 /* FediseerOpinionView.swift */,\n\t\t\t\t03B25B302CC4403500EB6DF5 /* Fediseer.swift */,\n\t\t\t);\n\t\t\tpath = Instance;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0311ADB52E4DF49100EC3120 /* Home */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t031DBA7A2F9A993000B4BAE4 /* SearchHomeListView.swift */,\n\t\t\t\t031DBA782F9A95B200B4BAE4 /* SearchHomeLabelStyle.swift */,\n\t\t\t\t031DBA762F9A93AD00B4BAE4 /* EventRowView.swift */,\n\t\t\t\t0302A87F2F9BC379000A127A /* SearchHomeCategoryLabelStyle.swift */,\n\t\t\t\t0311ADB62E4DF49800EC3120 /* SearchHomeView.swift */,\n\t\t\t\t0311ADBC2E4F668900EC3120 /* TopCommunitiesListView.swift */,\n\t\t\t\t0311ADBE2E4F68D000EC3120 /* TopPeopleListView.swift */,\n\t\t\t\t0311ADC02E4F693B00EC3120 /* TopInstancesListView.swift */,\n\t\t\t);\n\t\t\tpath = Home;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03134A492BEACF46002662CC /* Settings */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t81A179BB2DDE591700B17017 /* LongPressActionSettingsView.swift */,\n\t\t\t\tCD6DC02B2D86540500693B16 /* AnimatedAvatarSettingsView.swift */,\n\t\t\t\t0368F3672D7349C2007DEB70 /* Discussion Languages */,\n\t\t\t\tCD3485BC2D50156E006748B8 /* ZoomSliderSettingsView.swift */,\n\t\t\t\t039F58962C7B68F100C61658 /* AboutMlemView.swift */,\n\t\t\t\t03BF11C62D3D634A00CC1F66 /* AccessibilitySettingsView.swift */,\n\t\t\t\t03AB48542CBC0B8000567FF9 /* AccountAdvancedSettingsView.swift */,\n\t\t\t\t03AB48582CBC14CE00567FF9 /* AccountEmailSettingsView.swift */,\n\t\t\t\t03AB48512CBC042E00567FF9 /* AccountContentSettingsView.swift */,\n\t\t\t\t03D65D6F2F4B046F0041ADAF /* ContextMenuSettingsView.swift */,\n\t\t\t\t03134A572BEC1C46002662CC /* AccountListSettingsView.swift */,\n\t\t\t\t03AD0A812CFDBFA0001EF9F7 /* AccountLocalSettingsView.swift */,\n\t\t\t\t03AD0A832CFDC557001EF9F7 /* AccountNicknameFieldView.swift */,\n\t\t\t\t03267D832BED49CE009D6268 /* AccountSettingsView.swift */,\n\t\t\t\t03AB48562CBC0DFC00567FF9 /* AccountSignInSettingsView.swift */,\n\t\t\t\t039F588B2C7B574E00C61658 /* AdvancedSettingsView.swift */,\n\t\t\t\t037DE0742CE023E3007F7B92 /* BlockListView.swift */,\n\t\t\t\t039F58922C7B616600C61658 /* CommentSettingsView.swift */,\n\t\t\t\t03AB906E2D418C200054D9E1 /* CommentJumpButtonSettingsView.swift */,\n\t\t\t\t03BF11CB2D404F2C00CC1F66 /* CommentMaximumDepthSettingsView.swift */,\n\t\t\t\t0353948E2CA088E600795AA5 /* DeveloperSettingsView.swift */,\n\t\t\t\tCD5CAA0B2D41AE52008E20F2 /* EmbeddingSettingsView.swift */,\n\t\t\t\tCDEE15532D22364500EB9D7B /* ErrorLogView.swift */,\n\t\t\t\tCD45CB0C2D1880E3008BC729 /* FiltersSettingsView.swift */,\n\t\t\t\t039F58892C7B54FE00C61658 /* GeneralSettingsView.swift */,\n\t\t\t\t03ABE5E32DB8E57000374AFF /* AccountAgeVisibilitySettingsView.swift */,\n\t\t\t\t03A6315F2D4D1CBB009A47A6 /* HapticSettingsView.swift */,\n\t\t\t\t03A6315D2D4D1A1B009A47A6 /* DefaultFeedSettingsView.swift */,\n\t\t\t\t036FFA2E2D45197300998D8A /* PrivacySettingsView.swift */,\n\t\t\t\t038188982D43E0F30073E88D /* SafetySettingsView.swift */,\n\t\t\t\t03600D922D4531DF00C704CB /* PrivacyBypassImageProxySettingsView.swift */,\n\t\t\t\t0381889A2D4412A40073E88D /* SafetyBlurNsfwSettingsView.swift */,\n\t\t\t\t033819272D4424D9000AFC55 /* SafetyWarningsSettingsView.swift */,\n\t\t\t\tCDD4A09F2C8B985D0001AD1A /* ImportExportSettingsView.swift */,\n\t\t\t\t0325B9392D3A9E8100E28B97 /* InboxBadgeSettingsView.swift */,\n\t\t\t\t039F58942C7B618F00C61658 /* InboxSettingsView.swift */,\n\t\t\t\t03531EEB2C2D81DC004A3464 /* LinkSettingsView.swift */,\n\t\t\t\t038E5C122F6D617C00C54DEB /* ImageViewerSettingsView.swift */,\n\t\t\t\t038E5E882F6D694500C54DEB /* ImageViewerShowControlsSettingsView.swift */,\n\t\t\t\t038E5E8A2F6D6C3800C54DEB /* ImageViewerDismissSettingsView.swift */,\n\t\t\t\t03A630EE2D497143009A47A6 /* TappableLinksSettingsView.swift */,\n\t\t\t\t03A630EC2D497005009A47A6 /* ExternalLinkSettingsView.swift */,\n\t\t\t\t030056A32D7DB05A00EB0BA3 /* SharingLinksSettingsView.swift */,\n\t\t\t\t038028D72CACAB960091A8A2 /* ModeratorSettingsView.swift */,\n\t\t\t\t03F6BDF72D555F6E006A425E /* ModMailInteractionBarSettingsView.swift */,\n\t\t\t\t03A6316C2D4E3D24009A47A6 /* ModeratorActionSeparationSettingsView.swift */,\n\t\t\t\t03BF11C42D3D3AFB00CC1F66 /* PostReadIndicatorSettingsView.swift */,\n\t\t\t\tCDBFCB692C04EFFE008CD468 /* PostSettingsView.swift */,\n\t\t\t\t038E1AC92F59C18100D30F01 /* CommunitySettingsView.swift */,\n\t\t\t\t037F783E2D3BD05500D4E180 /* PostSettingsView+PostSizePicker.swift */,\n\t\t\t\t037F78442D3C25AB00D4E180 /* PostSubscriptionIndicatorSettingsView.swift */,\n\t\t\t\t037F78422D3C129B00D4E180 /* PostThumbnailSettingsView.swift */,\n\t\t\t\t0391E0F82CFF17AE0040CCA8 /* ProfileSettingsView.swift */,\n\t\t\t\t031E2D5C2BEFCC630003BC45 /* SettingsView.swift */,\n\t\t\t\t036FFA2C2D45110C00998D8A /* ChangePasswordView.swift */,\n\t\t\t\t0397D46B2C67E583002C6CDC /* SortingSettingsView.swift */,\n\t\t\t\t03B72B6A2C28A0190023A6C4 /* SubscriptionListSettingsView.swift */,\n\t\t\t\t03D284052D2AEE3A00A6659B /* TabBarSettingsView.swift */,\n\t\t\t\t031E2D5A2BEFC9460003BC45 /* ThemeSettingsView.swift */,\n\t\t\t\t038E1ABF2F58C76A00D30F01 /* SwipeActionEditorView.swift */,\n\t\t\t\t039F588D2C7B598000C61658 /* Components */,\n\t\t\t\t033FCB262C5E3933007B7CD1 /* Icon */,\n\t\t\t\t0397D4982C6EA68A002C6CDC /* InteractionBarEditor */,\n\t\t\t);\n\t\t\tpath = Settings;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03134A4E2BEAD23A002662CC /* Views */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4ED8482BF1112A00EFA0A2 /* View Modifiers */,\n\t\t\t\t03134A4F2BEAD245002662CC /* NavigationLink+NavigationPage.swift */,\n\t\t\t\t03F9672A2CE221220081C9A3 /* Label+Profile1.swift */,\n\t\t\t\t03DD69432D4FEA0000F8950D /* PreviewModifier+SampleEnvironment.swift */,\n\t\t\t);\n\t\t\tpath = Views;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0315B1BC2C74C3CD006D4F82 /* CommentEditor */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03B431B12C44409D001A1EB5 /* CommentEditorView.swift */,\n\t\t\t\t0315B1BD2C74C3D6006D4F82 /* CommentEditorView+Logic.swift */,\n\t\t\t\t0315B1C02C74C71A006D4F82 /* CommentEditorView+Context.swift */,\n\t\t\t\t0397D4902C6CE871002C6CDC /* PostEditor */,\n\t\t\t);\n\t\t\tpath = CommentEditor;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03267D802BED4714009D6268 /* Avatar */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t034B947E2C091EDD00039AF4 /* ProfileHeaderView.swift */,\n\t\t\t\t03267D812BED489C009D6268 /* AvatarBannerView.swift */,\n\t\t\t\t03134A592BEC2253002662CC /* AvatarStackView.swift */,\n\t\t\t);\n\t\t\tpath = Avatar;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t033F844B2D18D8F400D87A9E /* MessageFeedView */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t033F84482D18D1F400D87A9E /* MessageFeedView.swift */,\n\t\t\t\t033F84502D196AFD00D87A9E /* MessageFeedView+Logic.swift */,\n\t\t\t\t033F844C2D18D90900D87A9E /* MessageBubbleView.swift */,\n\t\t\t);\n\t\t\tpath = MessageFeedView;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t033F84722D1C784600D87A9E /* Modlog */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t033F84702D1C784600D87A9E /* ModlogEntryView.swift */,\n\t\t\t\t033F84712D1C784600D87A9E /* ModlogView.swift */,\n\t\t\t\t03A814282ED1BCA90023E9E8 /* ModlogView+Filters.swift */,\n\t\t\t\t0305EBA92D32B3B80066E5AD /* ModlogView+Logic.swift */,\n\t\t\t);\n\t\t\tpath = Modlog;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t033F84C62C2B192F002E3EDF /* MlemStats */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t033F84C22C2B12AA002E3EDF /* InstanceSummary.swift */,\n\t\t\t\t033F84C72C2B193D002E3EDF /* MlemStats.swift */,\n\t\t\t);\n\t\t\tpath = MlemStats;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t033FCAF12C598406007B7CD1 /* Person */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDBE78C12F38DD89008B254C /* PersonStubResolutionPage.swift */,\n\t\t\t\t0382A7EF2C09F0F800C79DDA /* PersonView.swift */,\n\t\t\t\tCD7928252C73E73400FA712D /* PersonView+Logic.swift */,\n\t\t\t);\n\t\t\tpath = Person;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t033FCAF22C598435007B7CD1 /* Community */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDADDB262F49210A00A4214A /* CommunityStubResolutionPage.swift */,\n\t\t\t\tCD1C64142D34286E0006B3C1 /* CommunityView+Logic.swift */,\n\t\t\t\t035DFA222EB3F7240021DE8C /* CommunityAboutView.swift */,\n\t\t\t\t033FCAF32C59843E007B7CD1 /* CommunityView.swift */,\n\t\t\t\t03049A212C650B2C00FF6889 /* CommunityDetailsView.swift */,\n\t\t\t\t0397D4632C676CA8002C6CDC /* FeedSortPicker.swift */,\n\t\t\t\t036ED67C2D0B006C0018E5EA /* TopSortPicker.swift */,\n\t\t\t\t036ED67E2D0B9A520018E5EA /* CommunitySearchSortPicker.swift */,\n\t\t\t\t0391E0FD2D05B2DF0040CCA8 /* AdvancedSortView.swift */,\n\t\t\t\t036ED67A2D0B004D0018E5EA /* AdvancedSortView+SortButton.swift */,\n\t\t\t);\n\t\t\tpath = Community;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t033FCB262C5E3933007B7CD1 /* Icon */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t033FCB222C5E3933007B7CD1 /* AlternateIcon.swift */,\n\t\t\t\t033FCB232C5E3933007B7CD1 /* AlternateIconCell.swift */,\n\t\t\t\t033FCB242C5E3933007B7CD1 /* AlternateIconLabel.swift */,\n\t\t\t\t033FCB252C5E3933007B7CD1 /* IconSettingsView.swift */,\n\t\t\t);\n\t\t\tpath = Icon;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0348F98B2DDBB505006639CD /* Onboarding */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0348F98C2DDBB526006639CD /* OnboardingView.swift */,\n\t\t\t\t0377BE062DE645E100E38593 /* OnboardingRecommendInstanceView.swift */,\n\t\t\t\t0377BD742DE219A400E38593 /* OnboardingUsernameView.swift */,\n\t\t\t\t0377BE9A2DEA328900E38593 /* OnboardingEmailView.swift */,\n\t\t\t\t0377BE9E2DEA361600E38593 /* OnboardingModel.swift */,\n\t\t\t);\n\t\t\tpath = Onboarding;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03500C252BF694A800CAA076 /* Toast */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03500C262BF69D1D00CAA076 /* ToastModel.swift */,\n\t\t\t\t03500C2A2BF7F1B100CAA076 /* ToastOverlayView.swift */,\n\t\t\t\t03500C2C2BF7FC2500CAA076 /* ToastView.swift */,\n\t\t\t\t03500C212BF5594100CAA076 /* ToastType.swift */,\n\t\t\t\t0369B3522BFA514B001EFEDF /* ToastLocation.swift */,\n\t\t\t\t03500C232BF55D0E00CAA076 /* Toast.swift */,\n\t\t\t);\n\t\t\tpath = Toast;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03531EEF2C2DA291004A3464 /* Search */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0311ADB52E4DF49100EC3120 /* Home */,\n\t\t\t\t035EDEF72C2DF93C00F51144 /* Results */,\n\t\t\t\t035EDEEB2C2DE93E00F51144 /* SearchBar */,\n\t\t\t\t0389DDCE2C39CB0E0005B808 /* SearchView.swift */,\n\t\t\t\t03D283FD2D25EEC500A6659B /* SearchView+Views.swift */,\n\t\t\t\t0320B6602C8DFCF100D38548 /* SearchView+FiltersView.swift */,\n\t\t\t\t03BF11C22D3D135D00CC1F66 /* SearchView+CreatorPicker.swift */,\n\t\t\t\t038028F72CB097A10091A8A2 /* SearchView+InstancePicker.swift */,\n\t\t\t\t038028F92CB097CB0091A8A2 /* SearchView+LocationPicker.swift */,\n\t\t\t\t038028F52CB096960091A8A2 /* SearchView+FilterModels.swift */,\n\t\t\t\t0320B6682C93506300D38548 /* SearchView+Logic.swift */,\n\t\t\t\t0389DDD22C39E4D40005B808 /* PasteLinkButtonView.swift */,\n\t\t\t\t03531EED2C2D9298004A3464 /* SearchSheetView.swift */,\n\t\t\t\t03531EF02C2DA298004A3464 /* SearchResultsView.swift */,\n\t\t\t\t035EDF002C2ECFE000F51144 /* Searchable.swift */,\n\t\t\t\t03D283F92D256E1E00A6659B /* VisitHistory.swift */,\n\t\t\t\t03D283FB2D25A3F700A6659B /* VisitHistory+CodedData.swift */,\n\t\t\t);\n\t\t\tpath = Search;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t035BE0852BDD8D9100F77D73 /* Navigation */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t035BE0862BDD8DA000F77D73 /* NavigationRootView.swift */,\n\t\t\t\t035BE08C2BDE88EC00F77D73 /* NavigationLayerView.swift */,\n\t\t\t\t035BE08A2BDD903100F77D73 /* NavigationModel.swift */,\n\t\t\t\t035BE08E2BDE911900F77D73 /* NavigationLayer.swift */,\n\t\t\t\t035BE0882BDD901B00F77D73 /* NavigationPage.swift */,\n\t\t\t\t03D001DC2EC53A97001BF97D /* NavigationPage+PresentationDetents.swift */,\n\t\t\t\t0320B6642C91DBD500D38548 /* NavigationPage+View.swift */,\n\t\t\t\t03531EF42C2DA610004A3464 /* NavigationSearchType.swift */,\n\t\t\t\t035BE0902BDEA01E00F77D73 /* View+NavigationSheetModifiers.swift */,\n\t\t\t\t03134A512BEAD69F002662CC /* SettingsPage.swift */,\n\t\t\t\t03CCDA9F2BF2795300C0C851 /* LoginPage.swift */,\n\t\t\t);\n\t\t\tpath = Navigation;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t035EDEEB2C2DE93E00F51144 /* SearchBar */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t035EDEEC2C2DE94B00F51144 /* _assignIfNotEqual.swift */,\n\t\t\t\t035EDEED2C2DE94B00F51144 /* DefaultTextInputType.swift */,\n\t\t\t\t035EDEEE2C2DE94B00F51144 /* SearchBar.swift */,\n\t\t\t\t031CA4AD2E58A84E00CF0C0F /* View+WithSheetSearch.swift */,\n\t\t\t\t035EDEEF2C2DE94B00F51144 /* SearchBar+NavigationView.swift */,\n\t\t\t\t035EDEF02C2DE94B00F51144 /* SearchBarExtensions.swift */,\n\t\t\t);\n\t\t\tpath = SearchBar;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t035EDEF72C2DF93C00F51144 /* Results */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t035EDEFA2C2DF98700F51144 /* CommunityListRowBody.swift */,\n\t\t\t\t035EDF022C2ED0DE00F51144 /* PersonListRowBody.swift */,\n\t\t\t\t0389DDD02C39E1030005B808 /* InstanceListRowBody.swift */,\n\t\t\t\t0389DDD42C39F1290005B808 /* CommunityListRow.swift */,\n\t\t\t\t03AFD0DE2C3B2E000054B8AD /* PersonListRow.swift */,\n\t\t\t\t03AFD0E02C3B30390054B8AD /* InstanceListRow.swift */,\n\t\t\t);\n\t\t\tpath = Results;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0368F3672D7349C2007DEB70 /* Discussion Languages */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD24CAFB2D5568F90032B5E8 /* DiscussionLanguageSettingsView.swift */,\n\t\t\t\t0368F3422D72796B007DEB70 /* LanguagePickerSheetView.swift */,\n\t\t\t\t0368F3682D7349D8007DEB70 /* LanguageListRowBody.swift */,\n\t\t\t);\n\t\t\tpath = \"Discussion Languages\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0369B3542BFA681B001EFEDF /* Inbox */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0369B3552BFA6824001EFEDF /* InboxView.swift */,\n\t\t\t\t034690982D105DFD0073E664 /* InboxView+Types.swift */,\n\t\t\t\t0353948A2CA076D000795AA5 /* InboxView+Views.swift */,\n\t\t\t\t031CA0D82E4FBFD800CF0C0F /* MarkAllAsReadButton.swift */,\n\t\t\t);\n\t\t\tpath = Inbox;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0369B3592BFB86C6001EFEDF /* Account */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD9D243C2CC1DF55006E5F3F /* AccountType.swift */,\n\t\t\t\t0369B35A2BFB86E3001EFEDF /* Account.swift */,\n\t\t\t\t03D2A6382C00FAE000ED4FF2 /* UserAccount.swift */,\n\t\t\t\t03D2A63A2C010B7500ED4FF2 /* GuestAccount.swift */,\n\t\t\t);\n\t\t\tpath = Account;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t037386422BDAF574007492B5 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t81DE61C42F48AF44006E4C36 /* UniformTypeIdentifiers.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0382A7EE2C09F0F800C79DDA /* Pages */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD87BEBF2D51328B0099F190 /* Editors */,\n\t\t\t\tCD33CA512D3C18AE00106C8C /* ImageViewer+Views.swift */,\n\t\t\t\t033FCAF22C598435007B7CD1 /* Community */,\n\t\t\t\t033FCAF12C598406007B7CD1 /* Person */,\n\t\t\t\t030BCB192C3EA5E20037680F /* Instance */,\n\t\t\t\t0355F9452C150B2300605248 /* ExternalApiInfoView.swift */,\n\t\t\t\t03AF91DC2C1B23E500E56644 /* ImageViewer.swift */,\n\t\t\t\tCDD99C3B2C73F3FF0010367F /* DeleteAccountView.swift */,\n\t\t\t\t03B25B3A2CC44FFF00EB6DF5 /* UploadConfirmationView.swift */,\n\t\t\t\t03E46AD12D130681002589DB /* VotesListView.swift */,\n\t\t\t\t033F84722D1C784600D87A9E /* Modlog */,\n\t\t\t\t033F844B2D18D8F400D87A9E /* MessageFeedView */,\n\t\t\t);\n\t\t\tname = Pages;\n\t\t\tpath = Mlem/App/Views/Pages;\n\t\t\tsourceTree = SOURCE_ROOT;\n\t\t};\n\t\t0397D4902C6CE871002C6CDC /* PostEditor */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0397D48F2C6CE871002C6CDC /* PostEditorView.swift */,\n\t\t\t\t038028D42CAB479D0091A8A2 /* PostEditorView+Views.swift */,\n\t\t\t\t03ECD7202C8654BA00D48BF6 /* PostEditorView+Toolbar.swift */,\n\t\t\t\t0315B1C52C754802006D4F82 /* PostEditorView+Logic.swift */,\n\t\t\t\t03ECD7182C81195000D48BF6 /* PostEditorView+LinkView.swift */,\n\t\t\t\t03ECD71A2C811D6700D48BF6 /* PostEditorView+ImageView.swift */,\n\t\t\t\t0397D4922C6CE87E002C6CDC /* PostEditorTargetView.swift */,\n\t\t\t\t03EC84412E959AA5004698BB /* PostEditorWebsitePreviewView.swift */,\n\t\t\t\t034A82022EBA688F00E5F904 /* LinkEditorView.swift */,\n\t\t\t);\n\t\t\tpath = PostEditor;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0397D4982C6EA68A002C6CDC /* InteractionBarEditor */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDF8C1902D5D501E00295CBA /* InteractionBarWidgetPickerView.swift */,\n\t\t\t\tCD64364F2D483C8F002668FB /* InteractionBarEditorView+Views.swift */,\n\t\t\t\t0397D4992C6EA6EE002C6CDC /* InteractionBarEditorView.swift */,\n\t\t\t\t03036C822C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift */,\n\t\t\t);\n\t\t\tpath = InteractionBarEditor;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t039D75652C4FC56A004F24C2 /* Images */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD13CC682C5D3CCA001AF428 /* Wrappers */,\n\t\t\t\tCD13CC672C5D3CBE001AF428 /* Core */,\n\t\t\t\tCD13CC662C5D3CA7001AF428 /* Helpers */,\n\t\t\t);\n\t\t\tpath = Images;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t039F588D2C7B598000C61658 /* Components */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD44C92B2E5CC8B600F24AC8 /* ConditionalLabelStyleViewModifier.swift */,\n\t\t\t\t039F58872C7B531800C61658 /* SquircleLabelStyle.swift */,\n\t\t\t\t039F588E2C7B599800C61658 /* ThemeLabel.swift */,\n\t\t\t\t0325B93D2D3AAE9E00E28B97 /* SettingsHeaderView.swift */,\n\t\t\t\t037F77EC2D3B064B00D4E180 /* SettingsDeviceView.swift */,\n\t\t\t\t037F78402D3C00E600D4E180 /* SettingsInteractionBarSummaryView.swift */,\n\t\t\t\t03BF11C92D4027E900CC1F66 /* DevicePickerItem.swift */,\n\t\t\t);\n\t\t\tpath = Components;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03A630F22D4976DE009A47A6 /* ShieldsBadgeView */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03A630F02D497674009A47A6 /* ShieldsBadgeView.swift */,\n\t\t\t\t03A630F32D4976EB009A47A6 /* ShieldsBadgeView+Logic.swift */,\n\t\t\t);\n\t\t\tpath = ShieldsBadgeView;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03A631C92D4FCEE3009A47A6 /* MlemMiddleware Mock */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03A631CA2D4FCEF7009A47A6 /* Post+Mock.swift */,\n\t\t\t\t038C85682D861A2100543F70 /* Comment+Mock.swift */,\n\t\t\t\t03DD69412D4FDE8900F8950D /* Person+Mock.swift */,\n\t\t\t\t03F6BDAC2D516615006A425E /* Community+Mock.swift */,\n\t\t\t\t03F6BD932D500DED006A425E /* PersonMockType.swift */,\n\t\t\t\t03F6BDAE2D516636006A425E /* CommunityMockType.swift */,\n\t\t\t\t03F6BDB02D52AA00006A425E /* CommunityMockType+Realistic.swift */,\n\t\t\t\t0370299C2D6B70F400B749DF /* MockApiClient+Realistic.swift */,\n\t\t\t\t03F6BD972D500E2A006A425E /* PersonMockType+Realistic.swift */,\n\t\t\t\t03F6BD9B2D501478006A425E /* ActorIdentifier+Mock.swift */,\n\t\t\t\t03F6BDBB2D52B7FE006A425E /* PostMockType.swift */,\n\t\t\t\t0370299E2D6B743B00B749DF /* PostMockType+Realistic.swift */,\n\t\t\t\t038C85E52D88337100543F70 /* CommentMockType.swift */,\n\t\t\t);\n\t\t\tpath = \"MlemMiddleware Mock\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03AF91E62C1C65AE00E56644 /* Interaction */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0381F7132F670F95008A7731 /* SwipeActionConfiguration.swift */,\n\t\t\t\t038E62DF2F6F0FC600C54DEB /* ContextMenuConfiguration.swift */,\n\t\t\t\t038E1ABB2F58B9EF00D30F01 /* CommunityActionConfiguration.swift */,\n\t\t\t\t0397D49B2C6EA73C002C6CDC /* InteractionBarConfiguration.swift */,\n\t\t\t\t03AF91E42C1C61FA00E56644 /* PostBarConfiguration.swift */,\n\t\t\t\t0381F7152F671427008A7731 /* PostBarConfiguration+Types.swift */,\n\t\t\t\t033F84BC2C2ACC5F002E3EDF /* CommentBarConfiguration.swift */,\n\t\t\t\t0381F7232F672258008A7731 /* CommentBarConfiguration+Types.swift */,\n\t\t\t\t032C32172C36F70300595286 /* ReplyBarConfiguration.swift */,\n\t\t\t\t0381F7272F6724D3008A7731 /* ReplyBarConfiguration+Types.swift */,\n\t\t\t\t03D662A42F5377630041ADAF /* ActionSeedSections.swift */,\n\t\t\t);\n\t\t\tpath = Interaction;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03B0EB6D2C87673D00F79FDF /* ExpandedPost */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDCF5A572F1426A0006748E8 /* CommentStubResolutionPage.swift */,\n\t\t\t\t0300309C2C4163C9009A65FF /* CommentTreeTracker.swift */,\n\t\t\t\t03E0EF442CA74036002CB66C /* CommentPage.swift */,\n\t\t\t\t03B0EB6E2C87827A00F79FDF /* ExpandedPostView.swift */,\n\t\t\t\t033EF40F2CB9AEF7004D8A3F /* ExpandedPostView+Views.swift */,\n\t\t\t\t03AD09E72CF88007001EF9F7 /* MoreRepliesButton.swift */,\n\t\t\t\t039F58852C7A810100C61658 /* ExpandedPostView+Logic.swift */,\n\t\t\t\t03E0EF422CA73D7A002CB66C /* PostStubResolutionPage.swift */,\n\t\t\t\t034147FC2D8F5844005503AF /* ExpandedPostHistoryTracker.swift */,\n\t\t\t);\n\t\t\tpath = ExpandedPost;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03D2A6352C00F91A00ED4FF2 /* Session */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03D2A6362C00F92400ED4FF2 /* Session.swift */,\n\t\t\t\t03D2A63C2C010CD400ED4FF2 /* UserSession.swift */,\n\t\t\t\t03D2A63E2C010DBF00ED4FF2 /* GuestSession.swift */,\n\t\t\t);\n\t\t\tpath = Session;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03D2A6402C011F3E00ED4FF2 /* ListRow */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D58B82B86D9F800B82964 /* AccountListRow.swift */,\n\t\t\t\t03D2A6412C011F4A00ED4FF2 /* AccountListRowBody.swift */,\n\t\t\t);\n\t\t\tpath = ListRow;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03D3A1EB2BB8CDFB009DE55E /* Action */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD3FC67F2D4A75050088E63B /* CounterApperance+StaticValues.swift */,\n\t\t\t\t03AF91E92C1CE96600E56644 /* Counter.swift */,\n\t\t\t\t0324FA762C1F0AE100F6247D /* Readout.swift */,\n\t\t\t\t03D3A1D32BB88EF1009DE55E /* Action.swift */,\n\t\t\t\t0397D4A12C6EB035002C6CDC /* ActionAppearance.swift */,\n\t\t\t\t0397D4A32C6FBC04002C6CDC /* ActionAppearance+StaticValues.swift */,\n\t\t\t\t03036C732C71408700C6DA1D /* CounterAppearance.swift */,\n\t\t\t\t03D3A1F02BB9D48E009DE55E /* BasicAction.swift */,\n\t\t\t\t03D3A1F22BB9D49B009DE55E /* ActionGroup.swift */,\n\t\t\t\t0389DDDA2C3AB6340005B808 /* ActionBuilder.swift */,\n\t\t\t\t03D3A1E42BB8B7A3009DE55E /* ActionType.swift */,\n\t\t\t\t038028D22CAB3D2D0091A8A2 /* ShareActivity.swift */,\n\t\t\t);\n\t\t\tpath = Action;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t03FA318D2C6FEF0E00D47FA3 /* InteractionBar */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03AF91E22C1C616F00E56644 /* InteractionBarView.swift */,\n\t\t\t\t03FA318E2C6FEF2000D47FA3 /* InteractionBarActionLabelView.swift */,\n\t\t\t);\n\t\t\tpath = InteractionBar;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6318EDC427EE4E0500BFCAE8 /* Models */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03D2A6352C00F91A00ED4FF2 /* Session */,\n\t\t\t\t03D3A1EB2BB8CDFB009DE55E /* Action */,\n\t\t\t\t0369B3592BFB86C6001EFEDF /* Account */,\n\t\t\t\tCD4D58CC2B86DDC300B82964 /* Settings */,\n\t\t\t\t03FE14032BF93FDD00A8377F /* ErrorDetails.swift */,\n\t\t\t\t033F84C62C2B192F002E3EDF /* MlemStats */,\n\t\t\t\t033F84B02C29907F002E3EDF /* FeedbackType.swift */,\n\t\t\t\t031EC52F2E5F77D7003408B7 /* FeedContext.swift */,\n\t\t\t\t033F84C02C2AD072002E3EDF /* CommentTreeNode.swift */,\n\t\t\t\t03ECD71E2C864DB700D48BF6 /* ImageUploadManager.swift */,\n\t\t\t\t031DBA6A2F9A8D6500B4BAE4 /* Events */,\n\t\t\t\t0320B65B2C8CFBDC00D38548 /* ImageUploadHistoryManager.swift */,\n\t\t\t\t03F6BD992D501041006A425E /* SeededRandomNumberGenerator.swift */,\n\t\t\t);\n\t\t\tpath = Models;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6363D5B827EE196700E34822 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03A6315C2D4D15F1009A47A6 /* PrivacyInfo.xcprivacy */,\n\t\t\t\t6363D5C327EE196700E34822 /* Mlem */,\n\t\t\t\t6363D5D927EE196A00E34822 /* MlemTests */,\n\t\t\t\t6363D5E327EE196A00E34822 /* MlemUITests */,\n\t\t\t\t81DE61DA2F48AF4C006E4C36 /* OpenInMlem */,\n\t\t\t\t6363D5C227EE196700E34822 /* Products */,\n\t\t\t\t037386422BDAF574007492B5 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6363D5C227EE196700E34822 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6363D5C127EE196700E34822 /* Mlem.app */,\n\t\t\t\t6363D5D627EE196A00E34822 /* MlemTests.xctest */,\n\t\t\t\t6363D5E027EE196A00E34822 /* MlemUITests.xctest */,\n\t\t\t\t81DE61C32F48AF44006E4C36 /* OpenInMlem.appex */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6363D5C327EE196700E34822 /* Mlem */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03DA26AC2D79F29700E66267 /* Packages */,\n\t\t\t\tCD4D583B2B867BB300B82964 /* App */,\n\t\t\t\t6363D5C827EE196A00E34822 /* Assets.xcassets */,\n\t\t\t\t030778EB2C52ED350018E61C /* Localizable.xcstrings */,\n\t\t\t\t630D753C27F65E44006E60C9 /* Info.plist */,\n\t\t\t\tB104A6E12A5AFC9F00B3E725 /* Mlem.entitlements */,\n\t\t\t\t03F6BDA12D502382006A425E /* Preview Content */,\n\t\t\t\t6332FDBC27EFAF7B0009A98A /* Settings.bundle */,\n\t\t\t);\n\t\t\tpath = Mlem;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6363D5D927EE196A00E34822 /* MlemTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D583E2B86855F00B82964 /* MlemTests.swift */,\n\t\t\t);\n\t\t\tpath = MlemTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6363D5E327EE196A00E34822 /* MlemUITests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D58402B86858100B82964 /* MlemUITests.swift */,\n\t\t\t);\n\t\t\tpath = MlemUITests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6363D5F327EE1BA900E34822 /* Views */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D58A82B86BE4C00B82964 /* Shared */,\n\t\t\t\tCD4D583C2B867C2900B82964 /* Root */,\n\t\t\t);\n\t\t\tpath = Views;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6363D5F427EE1BAE00E34822 /* Tabs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0369B3542BFA681B001EFEDF /* Inbox */,\n\t\t\t\t03134A492BEACF46002662CC /* Settings */,\n\t\t\t\tCD4BAD382B4C6C1B00A1E726 /* Feeds */,\n\t\t\t\t6DE1183A2A4A215F00810C7E /* Profile */,\n\t\t\t);\n\t\t\tpath = Tabs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6DE1183A2A4A215F00810C7E /* Profile */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6DE1183B2A4A217400810C7E /* Profile View.swift */,\n\t\t\t);\n\t\t\tpath = Profile;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t81DE61DA2F48AF4C006E4C36 /* OpenInMlem */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t81DE61D62F48AF4C006E4C36 /* Action.js */,\n\t\t\t\t81DE61D72F48AF4C006E4C36 /* ActionRequestHandler.swift */,\n\t\t\t\t81DE61D82F48AF4C006E4C36 /* Info.plist */,\n\t\t\t\t81DE61D92F48AF4C006E4C36 /* Media.xcassets */,\n\t\t\t\t81C4B4322F493C5E001406A1 /* InfoPlist.xcstrings */,\n\t\t\t);\n\t\t\tpath = OpenInMlem;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD13CC662C5D3CA7001AF428 /* Helpers */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDB95FCC2EBD3212008669D9 /* SmallOverlayButtonLabel.swift */,\n\t\t\t\tCD5C197B2D9709660089614C /* ZoomRecognizer */,\n\t\t\t\tCD57AFA42D0377E400AB3956 /* AnimationControlLayer.swift */,\n\t\t\t\tCD332D782CA7175200A53988 /* PlayButton.swift */,\n\t\t\t\tCD9857A32C5E7F9D0084C71F /* NsfwOverlayView.swift */,\n\t\t\t);\n\t\t\tpath = Helpers;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD13CC672C5D3CBE001AF428 /* Core */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD1DF6372D38357100F7851E /* MediaView.swift */,\n\t\t\t\tCD1DF63D2D387E8300F7851E /* MediaView+Views.swift */,\n\t\t\t\tCD7AC2CB2D7FDFF700A671B7 /* MediaView+Logic.swift */,\n\t\t\t);\n\t\t\tpath = Core;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD13CC682C5D3CCA001AF428 /* Wrappers */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD2783992D9B365D00DD4C69 /* ZoomableImageView.swift */,\n\t\t\t\t03B04FBF2C5FC32300824128 /* SimpleAvatarView.swift */,\n\t\t\t\tCDB2EC892BFAEFDF00DBC0EF /* ThumbnailImageView.swift */,\n\t\t\t\tCD13CC642C5D2B9D001AF428 /* CircleCroppedImageView.swift */,\n\t\t\t);\n\t\t\tpath = Wrappers;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD14461F2A5B328600610EF1 /* Data */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD1446202A5B328E00610EF1 /* Privacy Policy.swift */,\n\t\t\t\tCD1446242A5B357900610EF1 /* Document.swift */,\n\t\t\t\tCD1446262A5B36DA00610EF1 /* EULA.swift */,\n\t\t\t\tAD1B0D362A5F7A260006F554 /* Licenses.swift */,\n\t\t\t);\n\t\t\tpath = Data;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD1464522D63FE6F00202619 /* Legacy */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDE1F18E2C63D75A008AF042 /* LegacySettings.swift */,\n\t\t\t);\n\t\t\tpath = Legacy;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD317D4D2BE97FFB008F63E2 /* Colors */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03A9FD172D7CFC20007A734D /* Palette+Oled.swift */,\n\t\t\t\t03A9FD192D7CFC69007A734D /* Palette+Monochrome.swift */,\n\t\t\t\t03A9FD1D2D7CFF0D007A734D /* Palette+Dracula.swift */,\n\t\t\t\t03A9FD1B2D7CFD5C007A734D /* Palette+Solarized.swift */,\n\t\t\t);\n\t\t\tpath = Colors;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4BAD382B4C6C1B00A1E726 /* Feeds */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD7928212C73CB9B00FA712D /* Components */,\n\t\t\t\tCD7DB9742C4D6BEF00DCC542 /* Feed Comments */,\n\t\t\t\tCD4BAD342B4B2C0B00A1E726 /* FeedsView.swift */,\n\t\t\t\t031E2D502BEF961D0003BC45 /* SubscriptionListView.swift */,\n\t\t\t\t035394852C9CDC1100795AA5 /* SubscriptionListNavigationButton.swift */,\n\t\t\t\t03DAEA762C64074E0064DE64 /* SubscriptionListItemView.swift */,\n\t\t\t\tCD9CFA2D2C2E22E200739BBC /* Feed Header */,\n\t\t\t\tCDE021AB2BFA43220052FD61 /* Feed Posts */,\n\t\t\t\t033F84AC2C298466002E3EDF /* SectionIndexTitles.swift */,\n\t\t\t\t0311ADB82E4E0E0800EC3120 /* VisitAgainView.swift */,\n\t\t\t);\n\t\t\tpath = Feeds;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D583B2B867BB300B82964 /* App */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03EC85ED2E9D925B004698BB /* Actions */,\n\t\t\t\tCD1464522D63FE6F00202619 /* Legacy */,\n\t\t\t\tCDE1F18D2C63D646008AF042 /* Configuration */,\n\t\t\t\tCD14461F2A5B328600610EF1 /* Data */,\n\t\t\t\tCD4D58C62B86DCE500B82964 /* Enums */,\n\t\t\t\tCD4D58C22B86DC5800B82964 /* Extensions */,\n\t\t\t\tCD4D58942B86BA9E00B82964 /* Globals */,\n\t\t\t\tCD4D59112B87B35D00B82964 /* Logic */,\n\t\t\t\t6318EDC427EE4E0500BFCAE8 /* Models */,\n\t\t\t\tCD4D58C92B86DD3200B82964 /* Protocols */,\n\t\t\t\t6363D5F327EE1BA900E34822 /* Views */,\n\t\t\t);\n\t\t\tpath = App;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D583C2B867C2900B82964 /* Root */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tB1B78D632A51D53900F72485 /* AppDelegate.swift */,\n\t\t\t\t6363D5C627EE196700E34822 /* ContentView.swift */,\n\t\t\t\t037029A22D6B9B8400B749DF /* ContentView+Tab.swift */,\n\t\t\t\t039F58902C7B5C7A00C61658 /* ContentView+Logic.swift */,\n\t\t\t\t6363D5C427EE196700E34822 /* MlemApp.swift */,\n\t\t\t\t038096602C10AAD8003ED1D8 /* TransitionView.swift */,\n\t\t\t\tCDA1E81A2B8FC39A007953EF /* Login */,\n\t\t\t\t6363D5F427EE1BAE00E34822 /* Tabs */,\n\t\t\t);\n\t\t\tpath = Root;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D58672B86B38600B82964 /* Dependencies */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D58B42B86BFFA00B82964 /* PersistenceRepository+Dependency.swift */,\n\t\t\t);\n\t\t\tpath = Dependencies;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D58942B86BA9E00B82964 /* Globals */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D59212B87BD0800B82964 /* Definitions */,\n\t\t\t\tCD4D58672B86B38600B82964 /* Dependencies */,\n\t\t\t);\n\t\t\tpath = Globals;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D58A82B86BE4C00B82964 /* Shared */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03B431C32C45BA45001A1EB5 /* AccountPickerMenu.swift */,\n\t\t\t\tCD635E1A2C94DACD00864F75 /* BypassProxyWarningSheet.swift */,\n\t\t\t\t0397D45F2C66113F002C6CDC /* CommentBodyView.swift */,\n\t\t\t\t033F84BA2C2ACB96002E3EDF /* CommentView.swift */,\n\t\t\t\tCD43E8B22BF2C24E007C3D71 /* ContentLoader.swift */,\n\t\t\t\t030FF67E2BC8544600F6BFAC /* CustomTabBarController.swift */,\n\t\t\t\t030FF67C2BC8524500F6BFAC /* CustomTabItem.swift */,\n\t\t\t\t030FF67A2BC8521600F6BFAC /* CustomTabView.swift */,\n\t\t\t\t030FF6802BC859FD00F6BFAC /* CustomTabViewHostingController.swift */,\n\t\t\t\tCD7882AA2C013005002E1A30 /* EllipsisMenu.swift */,\n\t\t\t\tCD79281E2C73B52A00FA712D /* EndOfFeedView.swift */,\n\t\t\t\t03FE14072BF94FFB00A8377F /* ErrorView.swift */,\n\t\t\t\t034B948D2C0937BA00039AF4 /* FancyScrollView.swift */,\n\t\t\t\t03D284012D29E03C00A6659B /* FeedFilterButtonStyle.swift */,\n\t\t\t\t038C85D72D87696F00543F70 /* FeedToolbarOptions.swift */,\n\t\t\t\t037DE0792CE108D9007F7B92 /* FooterLinkView.swift */,\n\t\t\t\t033F84CB2C2B1E46002E3EDF /* HandleThreadiverseLinksModifier.swift */,\n\t\t\t\t0391E0FA2D0066240040CCA8 /* ImageUploadMenu.swift */,\n\t\t\t\t0324FA7A2C1F2CD200F6247D /* InfoStackView.swift */,\n\t\t\t\t039F58802C7A7E5900C61658 /* JumpButtonView.swift */,\n\t\t\t\t03EC83EF2E9590D3004698BB /* LinkHostView.swift */,\n\t\t\t\t03B431C12C45BA00001A1EB5 /* MarkdownEditorToolbarView.swift */,\n\t\t\t\t03B431B32C4481C3001A1EB5 /* MarkdownTextEditor.swift */,\n\t\t\t\t03AB484E2CBAE33500567FF9 /* MarkdownWithLinkList.swift */,\n\t\t\t\t03D3A1EE2BB9CA1D009DE55E /* MenuButton.swift */,\n\t\t\t\t0316CD632C382A6A009EA8EA /* MessageView.swift */,\n\t\t\t\t033F84652D1C780900D87A9E /* ModlogButtonView.swift */,\n\t\t\t\tCD77437E2C1BA5CE0085BB43 /* MultiplatformView.swift */,\n\t\t\t\tCD7DB9702C49C17200DCC542 /* PersonContentGridView.swift */,\n\t\t\t\t035DF9102EB2AA160021DE8C /* PersonContentGridView+FeedLoaderType.swift */,\n\t\t\t\t038028D92CACACD30091A8A2 /* PostEllipsisMenus.swift */,\n\t\t\t\tCD9CFA362C306DAB00739BBC /* PostGridView.swift */,\n\t\t\t\t0382A7F12C0A758E00C79DDA /* ProfileDateView.swift */,\n\t\t\t\tCD0374292D1DBFCF001E85FA /* ReadCheck.swift */,\n\t\t\t\t038028FE2CB72AC90091A8A2 /* ReasonShortcutView.swift */,\n\t\t\t\t030030A02C416B0B009A65FF /* RefreshPopupView.swift */,\n\t\t\t\t0305EBB12D35C1B70066E5AD /* RegistrationApplicationView.swift */,\n\t\t\t\t032C32152C36F65500595286 /* ReplyView.swift */,\n\t\t\t\t030050D22D109B7E002B1E99 /* ReportView.swift */,\n\t\t\t\t03B62B762CE295530077E9C8 /* RulesListView.swift */,\n\t\t\t\t03B62B782CE2A2C00077E9C8 /* RulesPickerView.swift */,\n\t\t\t\t032C32092C34495D00595286 /* SelectTextView.swift */,\n\t\t\t\t030056BA2D7E137800EB0BA3 /* ShareInstancePickerView.swift */,\n\t\t\t\tCD0E07002C12707700445849 /* ToolbarEllipsisMenu.swift */,\n\t\t\t\t034CC02F2D22C5BE00C557D3 /* WarningOverlayView.swift */,\n\t\t\t\tCDD99C3D2C73F4380010367F /* WarningView.swift */,\n\t\t\t\tCD13CC582C583C7A001AF428 /* WebsitePreviewView.swift */,\n\t\t\t\tCD13CC5A2C588B34001AF428 /* WebView.swift */,\n\t\t\t\tCD4D58AB2B86BE6100B82964 /* Accounts */,\n\t\t\t\t03267D802BED4714009D6268 /* Avatar */,\n\t\t\t\tCD869FCA2C15F8A100FC8B5B /* Bubble Picker */,\n\t\t\t\t03B0EB6D2C87673D00F79FDF /* ExpandedPost */,\n\t\t\t\tCDBEC30D2E84C9DB00F30B00 /* ExportableViews */,\n\t\t\t\t03049A182C6502DB00FF6889 /* Form */,\n\t\t\t\t039D75652C4FC56A004F24C2 /* Images */,\n\t\t\t\t03FA318D2C6FEF0E00D47FA3 /* InteractionBar */,\n\t\t\t\tCDB2EC842BFADD5E00DBC0EF /* Labels */,\n\t\t\t\t03D2A6402C011F3E00ED4FF2 /* ListRow */,\n\t\t\t\t035BE0852BDD8D9100F77D73 /* Navigation */,\n\t\t\t\t0382A7EE2C09F0F800C79DDA /* Pages */,\n\t\t\t\tCDAA02E42C82236200D75633 /* Palette Components */,\n\t\t\t\t03531EEF2C2DA291004A3464 /* Search */,\n\t\t\t\t03A630F22D4976DE009A47A6 /* ShieldsBadgeView */,\n\t\t\t\t03500C252BF694A800CAA076 /* Toast */,\n\t\t\t\tCDC71F242F15A6B900D314B1 /* ExpectedViews */,\n\t\t\t);\n\t\t\tpath = Shared;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D58AB2B86BE6100B82964 /* Accounts */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D58A92B86BE5900B82964 /* AccountListView.swift */,\n\t\t\t\tCD4D58AC2B86BE7100B82964 /* QuickSwitcherView.swift */,\n\t\t\t\tCD4D58BA2B86DA7D00B82964 /* AccountListView+Logic.swift */,\n\t\t\t);\n\t\t\tpath = Accounts;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D58C22B86DC5800B82964 /* Extensions */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t031DBA672F9A65DC00B4BAE4 /* BackendClient+Extensions.swift */,\n\t\t\t\tCD737FB92F771BF100E46411 /* InstanceSummarySoftware+Extensions.swift */,\n\t\t\t\tCD03B5BD2F3BA16100AEF786 /* Blockable+Extensions.swift */,\n\t\t\t\tCDF60A002E998BB2005FA3F1 /* Data+Extensions.swift */,\n\t\t\t\tCDA67A9E2D9B329D00E5D17B /* CGPoint+Extensions.swift */,\n\t\t\t\tCDFF9A322D88C3BE009E02E2 /* CGFloat+Extensions.swift */,\n\t\t\t\tCD5D8E2A2D6D5AAE00AF5CE4 /* CGSize+Extensions.swift */,\n\t\t\t\t03A631C92D4FCEE3009A47A6 /* MlemMiddleware Mock */,\n\t\t\t\tCDAA02E02C817AAB00D75633 /* Color+Extensions.swift */,\n\t\t\t\tCD332D7D2CA7485D00A53988 /* String+Extensions.swift */,\n\t\t\t\t0397D47F2C693A88002C6CDC /* [BlockNode]+Extensions.swift */,\n\t\t\t\t03A82FA02C0D1E8500D01A5C /* ApiClient+Extensions.swift */,\n\t\t\t\t033FCAEB2C57DCCD007B7CD1 /* ListingType+Extensions.swift */,\n\t\t\t\t03049A1D2C6508F400FF6889 /* RegistrationMode+Extensions.swift */,\n\t\t\t\t0318BA9E2D72405F006CA71F /* PostSortType+Extensions.swift */,\n\t\t\t\t038C86562D888EC100543F70 /* CommentSortType+Extensions.swift */,\n\t\t\t\t0368F34E2D734066007DEB70 /* SearchSortType+Extensions.swift */,\n\t\t\t\t03A818AC2EDCDBA20023E9E8 /* FederationMode+Extensions.swift */,\n\t\t\t\t0368F34C2D733215007DEB70 /* SortTimeRange+Extensions.swift */,\n\t\t\t\tCDCA44B32C176A4700C092B3 /* Array+Extensions.swift */,\n\t\t\t\t0389DDC82C39658E0005B808 /* Binding+Extensions.swift */,\n\t\t\t\t039F58982C7B697D00C61658 /* Bundle+Extensions.swift */,\n\t\t\t\tCDAA02DA2C810DB200D75633 /* Calendar+Extensions.swift */,\n\t\t\t\t033FCAED2C57DD14007B7CD1 /* CaptchaDifficulty+Extensions.swift */,\n\t\t\t\t0382A7F32C0A76A900C79DDA /* Date+Extensions.swift */,\n\t\t\t\t034B94882C09360A00039AF4 /* Int+Extensions.swift */,\n\t\t\t\tCD869FCF2C15F92E00FC8B5B /* Int+Extensions.swift */,\n\t\t\t\t034B94822C09340A00039AF4 /* MarkdownConfiguration+Extensions.swift */,\n\t\t\t\tCD0E06F62C0E739F00445849 /* PostType+Extensions.swift */,\n\t\t\t\t0397D4712C68078F002C6CDC /* PrefetchingConfiguration+Extensions.swift */,\n\t\t\t\tCD4D59152B87B38C00B82964 /* UIApplication+Extensions.swift */,\n\t\t\t\t0343C0472D3AD6DB001CF709 /* Set+Extensions.swift */,\n\t\t\t\t03AF91E02C1B25DE00E56644 /* UIDevice+Extensions.swift */,\n\t\t\t\t03B431B52C454D49001A1EB5 /* UIImage+Extensions.swift */,\n\t\t\t\t03B431BF2C45ABFB001A1EB5 /* UITextView+Extensions.swift */,\n\t\t\t\tCD4D59172B87B3B000B82964 /* UIViewController+Extensions.swift */,\n\t\t\t\tCDA8A0052BE43B350022F7ED /* Content Models */,\n\t\t\t\t03134A4E2BEAD23A002662CC /* Views */,\n\t\t\t\t037331A32C9CB12D00C826E1 /* EnvironmentValues+Extensions.swift */,\n\t\t\t\tCD4E386C2C836F8C009B24F2 /* UIUserInterfaceStyle+Extensions.swift */,\n\t\t\t\t03ABE5B52DB79A0E00374AFF /* DateComponents+Extensions.swift */,\n\t\t\t\t0377BE3A2DE8E70D00E38593 /* HapticLevel+Extensions.swift */,\n\t\t\t\t0377BD782DE22D4E00E38593 /* UsernameValidity+Extensions.swift */,\n\t\t\t\t03D006312ECCBA95001BF97D /* QuickSwipeAction+Actions.swift */,\n\t\t\t\t031CA5742E5900C800CF0C0F /* SwipeConfiguration+Extensions.swift */,\n\t\t\t\t031CA5B42E590B0D00CF0C0F /* QuickSwipeAction+Extensions.swift */,\n\t\t\t\t032A22002EB2A4D100E8B346 /* PersonContentFeedLoader+Extensions.swift */,\n\t\t\t\t035DF9122EB2AF8E0021DE8C /* GetContentFilter+Extensions.swift */,\n\t\t\t);\n\t\t\tname = Extensions;\n\t\t\tpath = Utility/Extensions;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D58C62B86DCE500B82964 /* Enums */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t81A179BE2DDE5BF300B17017 /* TabBarLongPressAction.swift */,\n\t\t\t\tCD6DC0292D86513800693B16 /* AnimatedAvatarBehavior.swift */,\n\t\t\t\tCD2C86532D5556BE0034CD8A /* MlemError.swift */,\n\t\t\t\tCD3485BA2D501463006748B8 /* ZoomSliderLocation.swift */,\n\t\t\t\tCDC44A352D1CBC200030F01C /* ReadPostIndicator.swift */,\n\t\t\t\tCD4D58C72B86DCED00B82964 /* AvatarType.swift */,\n\t\t\t\t03CBD18C2C6120F600E870BC /* PersonFlair.swift */,\n\t\t\t\t03AF91E62C1C65AE00E56644 /* Interaction */,\n\t\t\t\tCDA683F72C77E577000C4486 /* NsfwBlurBehavior.swift */,\n\t\t\t\t039F58832C7A7F2C00C61658 /* CommentJumpButtonLocation.swift */,\n\t\t\t\t0320B6622C8F8D5A00D38548 /* InstanceSort.swift */,\n\t\t\t\t03E46ACA2D1216CE002589DB /* PostViewLinkType.swift */,\n\t\t\t);\n\t\t\tpath = Enums;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D58C92B86DD3200B82964 /* Protocols */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D58EA2B86E63300B82964 /* AssociatedColor.swift */,\n\t\t\t);\n\t\t\tpath = Protocols;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D58CC2B86DDC300B82964 /* Settings */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D58CD2B86DDC800B82964 /* Options */,\n\t\t\t);\n\t\t\tpath = Settings;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D58CD2B86DDC800B82964 /* Options */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D58CE2B86DDEC00B82964 /* AccountSortMode.swift */,\n\t\t\t\tCD4D58F72B87B0D100B82964 /* InternetSpeed.swift */,\n\t\t\t\tCDB2EC7A2BFADA8B00DBC0EF /* PostSize.swift */,\n\t\t\t\tCD7882A62BFFD1A3002E1A30 /* ThumbnailLocation.swift */,\n\t\t\t);\n\t\t\tpath = Options;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D59112B87B35D00B82964 /* Logic */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD8DB9392D22003D00EB0C7B /* Animations.swift */,\n\t\t\t\tCD4D59122B87B36300B82964 /* Networking */,\n\t\t\t\t03FE140B2BF953B000A8377F /* HandleError.swift */,\n\t\t\t\tCD10FA762C7A8622008985AD /* ImageSaver.swift */,\n\t\t\t\tCD5581DD2C7B8B820043FAC3 /* ImageFunctions.swift */,\n\t\t\t);\n\t\t\tpath = Logic;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D59122B87B36300B82964 /* Networking */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD4D59132B87B36B00B82964 /* InternetConnectionManager.swift */,\n\t\t\t);\n\t\t\tpath = Networking;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4D59212B87BD0800B82964 /* Definitions */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDEE15512D22190000EB9D7B /* ErrorsTracker.swift */,\n\t\t\t\tCD7BF9312D18F4EB0020F2C5 /* FiltersTracker.swift */,\n\t\t\t\tCD4D58B22B86BFD400B82964 /* AccountsTracker.swift */,\n\t\t\t\t036CC3AE2B8145C30098B6A1 /* AppState.swift */,\n\t\t\t\t0380965E2C10AA80003ED1D8 /* AppState+Transition.swift */,\n\t\t\t\tCD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */,\n\t\t\t\tCD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */,\n\t\t\t\t03A9FD1F2D7D0072007A734D /* PaletteOption.swift */,\n\t\t\t);\n\t\t\tpath = Definitions;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD4ED8482BF1112A00EFA0A2 /* View Modifiers */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD7C4E562F3B92A600ADCBDD /* View+ReloadOnAccountSwitch.swift */,\n\t\t\t\tCDE88C342E68938800183AE5 /* View+AccountSwitcherGesture.swift */,\n\t\t\t\tCDB738282CB8A69D005B11BB /* View+PaletteBorder.swift */,\n\t\t\t\t030EE3032D651A4100D58C2C /* View+Refreshable.swift */,\n\t\t\t\tCD4ED8492BF1113800EFA0A2 /* View+TabReselectConsumer.swift */,\n\t\t\t\t03A82FA22C0D1F2400D01A5C /* View+ExternalApiWarning.swift */,\n\t\t\t\t030E95E62C80A20A0045BC2C /* View+NavigationTransition.swift */,\n\t\t\t\t03EC83242E916C51004698BB /* View+SafeAreaBar.swift */,\n\t\t\t\t0335AE102D8991330094FFD9 /* View+HiddenNavigationTitle.swift */,\n\t\t\t\t036A84D72D99531400E95D50 /* View+ConditionalNavigationTitle.swift */,\n\t\t\t\tCD3153062C38421B00BC5FBE /* View+LoadFeed.swift */,\n\t\t\t\t03B72B662C2888EE0023A6C4 /* View+ContextMenu.swift */,\n\t\t\t\tCD1D31822C56D742001B434B /* View+WidthReader.swift */,\n\t\t\t\t033FCB3D2C5E7FA9007B7CD1 /* View+OutdatedFeedPopup.swift */,\n\t\t\t\tCD0F28092C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift */,\n\t\t\t\t0395BCF72D9C57DE00865B33 /* View+Background.swift */,\n\t\t\t\tCDFB8C682C7796020070845F /* View+DynamicBlur.swift */,\n\t\t\t\tCD1B2E202C7F84160075C7EA /* View+MarkReadOnScroll.swift */,\n\t\t\t\t03FD6CAF2C9B719100500FD6 /* View+PopupAnchor.swift */,\n\t\t\t\t03EC86452E9E9D4C004698BB /* PopupAnchorModel.swift */,\n\t\t\t\t037029A02D6B9A3900B749DF /* View+TabBarPreview.swift */,\n\t\t\t\t034065C22D83742900637308 /* View+NavigtionStackPreview.swift */,\n\t\t\t\t031CA5B62E599F7E00CF0C0F /* View+QuickSwipes.swift */,\n\t\t\t);\n\t\t\tpath = \"View Modifiers\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD5C197B2D9709660089614C /* ZoomRecognizer */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDCC7FF22D9B283400CE18DA /* ZoomRecognizerCoordinator+Logic.swift */,\n\t\t\t\tCDCC7FF02D9B27CE00CE18DA /* ZoomRecognizerCoordinator+GestureRecognition.swift */,\n\t\t\t\tCDCC7FEE2D9B1E6E00CE18DA /* CachedComputation.swift */,\n\t\t\t\tCDCC7FEC2D9AEDBE00CE18DA /* BridgeDragValue.swift */,\n\t\t\t\tCDCC1BA82D99BEA8006579DF /* ZoomRecognizerCoordinator.swift */,\n\t\t\t\tCDCC1BA62D99BDB0006579DF /* GestureRecognizers.swift */,\n\t\t\t\tCD5C197E2D970E570089614C /* MomentumStatus.swift */,\n\t\t\t\tCD5C197C2D97096D0089614C /* ZoomCurves.swift */,\n\t\t\t\tCD9BD5802D8F8F750006AB7F /* ZoomRecognizer.swift */,\n\t\t\t);\n\t\t\tpath = ZoomRecognizer;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD7928212C73CB9B00FA712D /* Components */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t814CEF622F44577A0090F812 /* HiddenReadBannerView.swift */,\n\t\t\t\tCD7928222C73CBA400FA712D /* TileScoreView.swift */,\n\t\t\t\t0353948C2CA080EB00795AA5 /* FeedWelcomeView.swift */,\n\t\t\t\t036A84542D98253400E95D50 /* UpdateBannerView.swift */,\n\t\t\t);\n\t\t\tpath = Components;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD7DB9742C4D6BEF00DCC542 /* Feed Comments */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD7DB9722C4AEDDE00DCC542 /* TileCommentView.swift */,\n\t\t\t\tCD7DB9752C4D6C0A00DCC542 /* FeedCommentView.swift */,\n\t\t\t);\n\t\t\tpath = \"Feed Comments\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD869FCA2C15F8A100FC8B5B /* Bubble Picker */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD869FCB2C15F8AC00FC8B5B /* BubblePickerView.swift */,\n\t\t\t\tCD869FCD2C15F90C00FC8B5B /* ChildSizeReader.swift */,\n\t\t\t);\n\t\t\tpath = \"Bubble Picker\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD87BEBF2D51328B0099F190 /* Editors */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t03B7F3342EEEC70F00B00F6A /* NoteEditorView.swift */,\n\t\t\t\tCD87BEC22D5132BB0099F190 /* FilterViolationWarning.swift */,\n\t\t\t\t0315B1BC2C74C3CD006D4F82 /* CommentEditor */,\n\t\t\t\t0397D4792C693444002C6CDC /* ReportEditorView.swift */,\n\t\t\t\t038028FC2CB72A2A0091A8A2 /* ContentRemovalEditorView.swift */,\n\t\t\t\t0372EC1F2D370F0200257095 /* RegistrationApplicationDenialEditorView.swift */,\n\t\t\t\t0331715D2CCD6D95002DA370 /* ContentPurgeEditorView.swift */,\n\t\t\t\t03F967262CE218110081C9A3 /* PersonBanEditorView.swift */,\n\t\t\t\t03B62C3F2CE811CB0077E9C8 /* PersonBanEditorView+Logic.swift */,\n\t\t\t\t035DFA242EB3FB550021DE8C /* CommunityDescriptionEditorView.swift */,\n\t\t\t);\n\t\t\tpath = Editors;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCD9CFA2D2C2E22E200739BBC /* Feed Header */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD9CFA2F2C2E22F600739BBC /* FeedDescription.swift */,\n\t\t\t\tCD9CFA2B2C2E1EF300739BBC /* FeedIconView.swift */,\n\t\t\t\tCD9CFA292C2E1E8400739BBC /* FeedHeaderView.swift */,\n\t\t\t);\n\t\t\tpath = \"Feed Header\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDA1E81A2B8FC39A007953EF /* Login */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0348F98B2DDBB505006639CD /* Onboarding */,\n\t\t\t\t039EFEC22BEEBEE0003AC372 /* LoginInstancePickerView.swift */,\n\t\t\t\t03C93CEF2BEFFB1A00327BFE /* LoginCredentialsView.swift */,\n\t\t\t\t03CCDAA32BF2852E00C0C851 /* LoginTotpView.swift */,\n\t\t\t\t0320B64E2C8A638A00D38548 /* SignUpView.swift */,\n\t\t\t\t0320B6572C8BB3C400D38548 /* SignUpView+Views.swift */,\n\t\t\t\t0320B6662C93504600D38548 /* SignUpView+Logic.swift */,\n\t\t\t\t0320B6592C8CBB0D00D38548 /* SignUpView+EmailConfirmationView.swift */,\n\t\t\t);\n\t\t\tpath = Login;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDA8A0052BE43B350022F7ED /* Content Models */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDADDB222F491D2F00A4214A /* Community */,\n\t\t\t\tCD2276D42F369C5F0024AEB1 /* Person */,\n\t\t\t\tCD6CD1222F25EC6A00566122 /* Comment */,\n\t\t\t\tCD6CD1212F25C36800566122 /* Post */,\n\t\t\t\tCD6CD11A2F25BF1300566122 /* Interactable */,\n\t\t\t\t032C32032C3439C600595286 /* ActorIdentifiable+Extensions.swift */,\n\t\t\t\t030056A52D7DBD4F00EB0BA3 /* Sharable+Extensions.swift */,\n\t\t\t\t0305EBAB2D32C9300066E5AD /* ModlogEntryType+Extensions.swift */,\n\t\t\t\t0320B6532C8B65EB00D38548 /* Captcha+Extensions.swift */,\n\t\t\t\t034B94802C09306D00039AF4 /* CommunityOrPersonStub+Extensions.swift */,\n\t\t\t\t039D75632C4EEE69004F24C2 /* DeletableProviding+Extensions.swift */,\n\t\t\t\t0389DDC42C38917A0005B808 /* InboxItemProviding+Extensions.swift */,\n\t\t\t\t0325B93B2D3AA62500E28B97 /* InboxItemType+Extensions.swift */,\n\t\t\t\t03D283FF2D26F09500A6659B /* Instance+Extensions.swift */,\n\t\t\t\t0389DDC22C38907C0005B808 /* Message1Providing+Extensions.swift */,\n\t\t\t\t033F84772D1D68D200D87A9E /* ModlogEntryContent+Extensions.swift */,\n\t\t\t\t03D0273B2CD3BA5100984519 /* PersonContent+Extensions.swift */,\n\t\t\t\t036ED6822D0C483B0018E5EA /* ProfileProviding+Extensions.swift */,\n\t\t\t\t033171772CCE89E3002DA370 /* PurgableProviding+Extensions.swift */,\n\t\t\t\t0372EBCD2D36FBCF00257095 /* RegistrationApplication+Extensions.swift */,\n\t\t\t\t034690922D0F4D720073E664 /* RemovableProviding+Extensions.swift */,\n\t\t\t\t030050D42D10AE30002B1E99 /* Report+Extensions.swift */,\n\t\t\t\t0397D4852C6A24D2002C6CDC /* ReportableProviding+Extensions.swift */,\n\t\t\t\t03E46AD32D130728002589DB /* ScoringOperation+Extensions.swift */,\n\t\t\t\t032C32072C34469900595286 /* SelectableContentProviding+Extensions.swift */,\n\t\t\t\t0389DDC62C389F840005B808 /* UnreadCount+Extensions.swift */,\n\t\t\t\tCD7743882C20EDEE0085BB43 /* VotesModel+Extensions.swift */,\n\t\t\t\t03ACE7192DFD57CC00FD66C0 /* SiteSoftware+Extensions.swift */,\n\t\t\t\t03B045F52E26D64900540EFB /* SiteSoftwareType+Extensions.swift */,\n\t\t\t);\n\t\t\tpath = \"Content Models\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDAA02E42C82236200D75633 /* Palette Components */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDAA02E22C821C9100D75633 /* Divider.swift */,\n\t\t\t\tCDD8B94B2C8234BC00510EBB /* Form.swift */,\n\t\t\t\tCDB41E892C83C24400BD2DE9 /* Section.swift */,\n\t\t\t\tCDD4A09D2C8B69FC0001AD1A /* Button.swift */,\n\t\t\t);\n\t\t\tpath = \"Palette Components\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDB2EC842BFADD5E00DBC0EF /* Labels */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDB2EC852BFADD7C00DBC0EF /* FullyQualifiedLabelView.swift */,\n\t\t\t\t03E614E62C0BCDC200F692A4 /* FullyQualifiedLinkView.swift */,\n\t\t\t\tCDB2EC872BFAE14800DBC0EF /* FullyQualifiedNameView.swift */,\n\t\t\t);\n\t\t\tpath = Labels;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDBEC30D2E84C9DB00F30B00 /* ExportableViews */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDD8E30C2EEA07EE00FC4C8D /* ExportableCommentLoader.swift */,\n\t\t\t\tCDA1D2A42ED8C4670077A9EA /* ExportableViewComponents.swift */,\n\t\t\t\tCD756A972ED766930031D7D1 /* ExportableCommentEditorView.swift */,\n\t\t\t\tCD756A952ED765E90031D7D1 /* ExportableCommentView.swift */,\n\t\t\t\tCD0C5C0F2E9962970074D5A4 /* ExportablePostEditorView.swift */,\n\t\t\t\tCDBEC30E2E84C9F300F30B00 /* ExportablePostView.swift */,\n\t\t\t);\n\t\t\tpath = ExportableViews;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDBFCB662C04EA59008CD468 /* Feed Post Components */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCD7882A82BFFDFC7002E1A30 /* PostTag.swift */,\n\t\t\t\tCDBFCB642C03920C008CD468 /* PostLinkHostView.swift */,\n\t\t\t\t0353949B2CA4B3E800795AA5 /* CrossPostListView.swift */,\n\t\t\t);\n\t\t\tpath = \"Feed Post Components\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDE021AB2BFA43220052FD61 /* Feed Posts */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDB2EC7C2BFADAB300DBC0EF /* CompactPostView.swift */,\n\t\t\t\tCD950E042F0ED6F7002A0595 /* FeedPostView.swift */,\n\t\t\t\t03E46ACC2D121C19002589DB /* HeadlinePostBodyView.swift */,\n\t\t\t\tCDB2EC7E2BFADACC00DBC0EF /* HeadlinePostView.swift */,\n\t\t\t\t03B431BB2C455838001A1EB5 /* LargePostBodyView.swift */,\n\t\t\t\tCDB2EC802BFADADF00DBC0EF /* LargePostView.swift */,\n\t\t\t\tCDBFCB6B2C054AA7008CD468 /* TilePostView.swift */,\n\t\t\t\tCDBFCB662C04EA59008CD468 /* Feed Post Components */,\n\t\t\t\t037352322F27A83900341673 /* PostPollView.swift */,\n\t\t\t);\n\t\t\tpath = \"Feed Posts\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDE1F18D2C63D646008AF042 /* Configuration */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDF9EF322AB2845C003F885B /* Icons.swift */,\n\t\t\t\tCD317D4D2BE97FFB008F63E2 /* Colors */,\n\t\t\t\tCDE1F19A2C63E293008AF042 /* Constants */,\n\t\t\t\tCDE1F1992C63E277008AF042 /* User Settings */,\n\t\t\t);\n\t\t\tpath = Configuration;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDE1F1992C63E277008AF042 /* User Settings */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDD4A09B2C8A122F0001AD1A /* Settings.swift */,\n\t\t\t\tCDB3DDFC2DA485D000F407AB /* SettingsValues.swift */,\n\t\t\t\tCDE1F19B2C63E2EB008AF042 /* SettingPropertyWrapper.swift */,\n\t\t\t\t036ED6782D0AF3740018E5EA /* PinnedSortTracker.swift */,\n\t\t\t);\n\t\t\tpath = \"User Settings\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDE1F19A2C63E293008AF042 /* Constants */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDE1F19F2C63E388008AF042 /* Platform Constants */,\n\t\t\t\tCDE1F19D2C63E306008AF042 /* Constants.swift */,\n\t\t\t);\n\t\t\tpath = Constants;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCDE1F19F2C63E388008AF042 /* Platform Constants */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCDE1F1932C63DF44008AF042 /* PlatformConstants.swift */,\n\t\t\t\tCDE1F1952C63DF89008AF042 /* PhoneConstants.swift */,\n\t\t\t\tCDE1F1972C63DFC9008AF042 /* PadConstants.swift */,\n\t\t\t);\n\t\t\tpath = \"Platform Constants\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXHeadersBuildPhase section */\n\t\t03E96D082BD6ED7E00B7A98F /* Headers */ = {\n\t\t\tisa = PBXHeadersBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXHeadersBuildPhase section */\n\n/* Begin PBXNativeTarget section */\n\t\t6363D5C027EE196700E34822 /* Mlem */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 6363D5EA27EE196A00E34822 /* Build configuration list for PBXNativeTarget \"Mlem\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t03E96D082BD6ED7E00B7A98F /* Headers */,\n\t\t\t\t6363D5BD27EE196700E34822 /* Sources */,\n\t\t\t\t50F830EB2A47CC4F00D67099 /* Swiftlint */,\n\t\t\t\t6363D5BE27EE196700E34822 /* Frameworks */,\n\t\t\t\t6363D5BF27EE196700E34822 /* Resources */,\n\t\t\t\t81DE61D12F48AF44006E4C36 /* Embed Foundation Extensions */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t81DE61CF2F48AF44006E4C36 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tfileSystemSynchronizedGroups = (\n\t\t\t\t031DBA6A2F9A8D6500B4BAE4 /* Events */,\n\t\t\t\t03DA26AC2D79F29700E66267 /* Packages */,\n\t\t\t\t03EC85ED2E9D925B004698BB /* Actions */,\n\t\t\t\t03F6BDA12D502382006A425E /* Preview Content */,\n\t\t\t\tCD2276D42F369C5F0024AEB1 /* Person */,\n\t\t\t\tCD6CD11A2F25BF1300566122 /* Interactable */,\n\t\t\t\tCD6CD1212F25C36800566122 /* Post */,\n\t\t\t\tCD6CD1222F25EC6A00566122 /* Comment */,\n\t\t\t\tCDADDB222F491D2F00A4214A /* Community */,\n\t\t\t\tCDC71F242F15A6B900D314B1 /* ExpectedViews */,\n\t\t\t);\n\t\t\tname = Mlem;\n\t\t\tpackageProductDependencies = (\n\t\t\t\t636250DB2A18111400FC59B4 /* KeychainAccess */,\n\t\t\t\tB104A6D72A59BF3C00B3E725 /* Nuke */,\n\t\t\t\tB104A6D92A59BF3C00B3E725 /* NukeExtensions */,\n\t\t\t\tB104A6DB2A59BF3C00B3E725 /* NukeUI */,\n\t\t\t\tB104A6DD2A59BF3C00B3E725 /* NukeVideo */,\n\t\t\t\t50C99B552A61D792005D57DD /* Dependencies */,\n\t\t\t\tCD4368C02AE23FD400BD8BD1 /* Semaphore */,\n\t\t\t\t030FF6782BC84F7E00F6BFAC /* SwiftUIIntrospect */,\n\t\t\t\t037386462BDAFE81007492B5 /* LemmyMarkdownUI */,\n\t\t\t\t03FA318B2C6FECAE00D47FA3 /* Flow */,\n\t\t\t\tCDE4AC462CA372B600981010 /* SDWebImageWebPCoder */,\n\t\t\t\t03A9FD152D7CEC09007A734D /* Theming */,\n\t\t\t\t0341480C2D8F63A6005503AF /* MlemMiddleware */,\n\t\t\t\t036879992DA1320000E796EF /* ComponentViews */,\n\t\t\t\tCDA711FA2DB5CAC3008BC3ED /* Media */,\n\t\t\t\t03D8BF422DA55B6900506687 /* Icons */,\n\t\t\t\t0377BF9A2DF0E0E000E38593 /* Rest */,\n\t\t\t\t0377BE282DE7A2DE00E38593 /* Haptics */,\n\t\t\t\t031CA5762E5900E900CF0C0F /* QuickSwipes */,\n\t\t\t\t03EC83ED2E958A44004698BB /* OpenGraph */,\n\t\t\t\t03EC85EB2E9D8F37004698BB /* Actions */,\n\t\t\t\t038100502F6AE867008A7731 /* MlemBackend */,\n\t\t\t\t0347A6FA2F97F4CF00EFD670 /* FediverseEvents */,\n\t\t\t);\n\t\t\tproductName = Mlem;\n\t\t\tproductReference = 6363D5C127EE196700E34822 /* Mlem.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n\t\t6363D5D527EE196A00E34822 /* MlemTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 6363D5ED27EE196A00E34822 /* Build configuration list for PBXNativeTarget \"MlemTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t6363D5D227EE196A00E34822 /* Sources */,\n\t\t\t\t6363D5D327EE196A00E34822 /* Frameworks */,\n\t\t\t\t6363D5D427EE196A00E34822 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t6363D5D827EE196A00E34822 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = MlemTests;\n\t\t\tproductName = MlemTests;\n\t\t\tproductReference = 6363D5D627EE196A00E34822 /* MlemTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t6363D5DF27EE196A00E34822 /* MlemUITests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 6363D5F027EE196A00E34822 /* Build configuration list for PBXNativeTarget \"MlemUITests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t6363D5DC27EE196A00E34822 /* Sources */,\n\t\t\t\t6363D5DD27EE196A00E34822 /* Frameworks */,\n\t\t\t\t6363D5DE27EE196A00E34822 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t6363D5E227EE196A00E34822 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = MlemUITests;\n\t\t\tproductName = MlemUITests;\n\t\t\tproductReference = 6363D5E027EE196A00E34822 /* MlemUITests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.ui-testing\";\n\t\t};\n\t\t81DE61C22F48AF44006E4C36 /* OpenInMlem */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 81DE61D52F48AF44006E4C36 /* Build configuration list for PBXNativeTarget \"OpenInMlem\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t81DE61BF2F48AF44006E4C36 /* Sources */,\n\t\t\t\t81DE61C02F48AF44006E4C36 /* Frameworks */,\n\t\t\t\t81DE61C12F48AF44006E4C36 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = OpenInMlem;\n\t\t\tpackageProductDependencies = (\n\t\t\t);\n\t\t\tproductName = OpenInMlem;\n\t\t\tproductReference = 81DE61C32F48AF44006E4C36 /* OpenInMlem.appex */;\n\t\t\tproductType = \"com.apple.product-type.app-extension\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t6363D5B927EE196700E34822 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = 1;\n\t\t\t\tLastSwiftUpdateCheck = 2620;\n\t\t\t\tLastUpgradeCheck = 1610;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t6363D5C027EE196700E34822 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 13.3;\n\t\t\t\t\t};\n\t\t\t\t\t6363D5D527EE196A00E34822 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 13.3;\n\t\t\t\t\t\tLastSwiftMigration = 1500;\n\t\t\t\t\t\tTestTargetID = 6363D5C027EE196700E34822;\n\t\t\t\t\t};\n\t\t\t\t\t6363D5DF27EE196A00E34822 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 13.3;\n\t\t\t\t\t\tLastSwiftMigration = 1500;\n\t\t\t\t\t\tTestTargetID = 6363D5C027EE196700E34822;\n\t\t\t\t\t};\n\t\t\t\t\t81DE61C22F48AF44006E4C36 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 26.2;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 6363D5BC27EE196700E34822 /* Build configuration list for PBXProject \"Mlem\" */;\n\t\t\tcompatibilityVersion = \"Xcode 13.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t\t\"en-GB\",\n\t\t\t\tfr,\n\t\t\t);\n\t\t\tmainGroup = 6363D5B827EE196700E34822;\n\t\t\tpackageReferences = (\n\t\t\t\t636250DA2A18111400FC59B4 /* XCRemoteSwiftPackageReference \"KeychainAccess\" */,\n\t\t\t\tB104A6D62A59BF3C00B3E725 /* XCRemoteSwiftPackageReference \"Nuke\" */,\n\t\t\t\t50C99B542A61D792005D57DD /* XCRemoteSwiftPackageReference \"swift-dependencies\" */,\n\t\t\t\tCD4368BF2AE23FD400BD8BD1 /* XCRemoteSwiftPackageReference \"Semaphore\" */,\n\t\t\t\t0392826E2BC84E480097F91A /* XCRemoteSwiftPackageReference \"SwiftUI-Introspect\" */,\n\t\t\t\t037386452BDAFE81007492B5 /* XCRemoteSwiftPackageReference \"LemmyMarkdownUI\" */,\n\t\t\t\t03FA318A2C6FEC5400D47FA3 /* XCRemoteSwiftPackageReference \"SwiftUI-Flow\" */,\n\t\t\t\tCDE4AC412CA3706F00981010 /* XCRemoteSwiftPackageReference \"SDWebImageWebPCoder\" */,\n\t\t\t\t03EC83EC2E958A44004698BB /* XCRemoteSwiftPackageReference \"OpenGraph\" */,\n\t\t\t);\n\t\t\tproductRefGroup = 6363D5C227EE196700E34822 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t6363D5C027EE196700E34822 /* Mlem */,\n\t\t\t\t6363D5D527EE196A00E34822 /* MlemTests */,\n\t\t\t\t6363D5DF27EE196A00E34822 /* MlemUITests */,\n\t\t\t\t81DE61C22F48AF44006E4C36 /* OpenInMlem */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t6363D5BF27EE196700E34822 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t6332FDBD27EFAF7C0009A98A /* Settings.bundle in Resources */,\n\t\t\t\t030778EC2C52ED350018E61C /* Localizable.xcstrings in Resources */,\n\t\t\t\t6363D5C927EE196A00E34822 /* Assets.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t6363D5D427EE196A00E34822 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t6363D5DE27EE196A00E34822 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t81DE61C12F48AF44006E4C36 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t81C4B4332F493C5E001406A1 /* InfoPlist.xcstrings in Resources */,\n\t\t\t\t81DE61DB2F48AF4C006E4C36 /* Action.js in Resources */,\n\t\t\t\t81DE61DD2F48AF4C006E4C36 /* Media.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t50F830EB2A47CC4F00D67099 /* Swiftlint */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = Swiftlint;\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"if [[ \\\"$(uname -m)\\\" == arm64 ]]; then\\n    export PATH=\\\"/opt/homebrew/bin:$PATH\\\"\\nfi\\n\\nif which swiftlint > /dev/null; then\\n  swiftlint lint --strict\\nelse\\n  echo \\\"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\\\"\\nfi\\n\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t6363D5BD27EE196700E34822 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t03DA4FB72CF115FB001C3C77 /* CommentEditorView.swift in Sources */,\n\t\t\t\t039F58822C7A7EF300C61658 /* ToolbarEllipsisMenu.swift in Sources */,\n\t\t\t\tCD332D792CA7175500A53988 /* PlayButton.swift in Sources */,\n\t\t\t\t03A9FD1E2D7CFF0D007A734D /* Palette+Dracula.swift in Sources */,\n\t\t\t\t03531EEC2C2D81DC004A3464 /* LinkSettingsView.swift in Sources */,\n\t\t\t\t038E5C132F6D617C00C54DEB /* ImageViewerSettingsView.swift in Sources */,\n\t\t\t\t03AB48552CBC0B8000567FF9 /* AccountAdvancedSettingsView.swift in Sources */,\n\t\t\t\t039F588A2C7B54FE00C61658 /* GeneralSettingsView.swift in Sources */,\n\t\t\t\t03F967272CE218110081C9A3 /* PersonBanEditorView.swift in Sources */,\n\t\t\t\t03AD0A822CFDBFA0001EF9F7 /* AccountLocalSettingsView.swift in Sources */,\n\t\t\t\t034A82032EBA688F00E5F904 /* LinkEditorView.swift in Sources */,\n\t\t\t\t039F58992C7B697D00C61658 /* Bundle+Extensions.swift in Sources */,\n\t\t\t\t035DFA252EB3FB550021DE8C /* CommunityDescriptionEditorView.swift in Sources */,\n\t\t\t\t033F84D92C2B61FB002E3EDF /* ToastType.swift in Sources */,\n\t\t\t\t03600D932D4531DF00C704CB /* PrivacyBypassImageProxySettingsView.swift in Sources */,\n\t\t\t\t03A82FA12C0D1E8500D01A5C /* ApiClient+Extensions.swift in Sources */,\n\t\t\t\t0397D47A2C693444002C6CDC /* ReportEditorView.swift in Sources */,\n\t\t\t\tCDA683F82C77E577000C4486 /* NsfwBlurBehavior.swift in Sources */,\n\t\t\t\t03B25B2F2CC43F8600EB6DF5 /* InstanceSafetyView.swift in Sources */,\n\t\t\t\tCDADDB272F49210A00A4214A /* CommunityStubResolutionPage.swift in Sources */,\n\t\t\t\t035DF9112EB2AA160021DE8C /* PersonContentGridView+FeedLoaderType.swift in Sources */,\n\t\t\t\t03E0EF452CA74036002CB66C /* CommentPage.swift in Sources */,\n\t\t\t\t030BCB1B2C3EA5FD0037680F /* InstanceDetailsView.swift in Sources */,\n\t\t\t\t03134A582BEC1C46002662CC /* AccountListSettingsView.swift in Sources */,\n\t\t\t\tCDCC1BA72D99BDB7006579DF /* GestureRecognizers.swift in Sources */,\n\t\t\t\t03E614E52C0BCCAA00F692A4 /* PostSize.swift in Sources */,\n\t\t\t\t03B72B672C2888EE0023A6C4 /* View+ContextMenu.swift in Sources */,\n\t\t\t\t81A179BF2DDE5BF300B17017 /* TabBarLongPressAction.swift in Sources */,\n\t\t\t\tCDD8E30D2EEA07F100FC4C8D /* ExportableCommentLoader.swift in Sources */,\n\t\t\t\t03B25B352CC4446400EB6DF5 /* FediseerOpinionListView.swift in Sources */,\n\t\t\t\tCDB2EC7D2BFADAB300DBC0EF /* CompactPostView.swift in Sources */,\n\t\t\t\tCD33CA522D3C18BF00106C8C /* ImageViewer+Views.swift in Sources */,\n\t\t\t\t031CA5B52E590B0D00CF0C0F /* QuickSwipeAction+Extensions.swift in Sources */,\n\t\t\t\t030056A42D7DB05A00EB0BA3 /* SharingLinksSettingsView.swift in Sources */,\n\t\t\t\t0315B1C12C74C71A006D4F82 /* CommentEditorView+Context.swift in Sources */,\n\t\t\t\t03D283FE2D25EEC500A6659B /* SearchView+Views.swift in Sources */,\n\t\t\t\t03AFD0DF2C3B2E000054B8AD /* PersonListRow.swift in Sources */,\n\t\t\t\t03A6315E2D4D1A1B009A47A6 /* DefaultFeedSettingsView.swift in Sources */,\n\t\t\t\t03DAEA772C64074E0064DE64 /* SubscriptionListItemView.swift in Sources */,\n\t\t\t\t0324FA772C1F0AE100F6247D /* Readout.swift in Sources */,\n\t\t\t\t037F783F2D3BD05500D4E180 /* PostSettingsView+PostSizePicker.swift in Sources */,\n\t\t\t\t0389DDCF2C39CB0E0005B808 /* SearchView.swift in Sources */,\n\t\t\t\t03A630F42D4976EB009A47A6 /* ShieldsBadgeView+Logic.swift in Sources */,\n\t\t\t\tCD1DF63E2D387E8500F7851E /* MediaView+Views.swift in Sources */,\n\t\t\t\t03FE14042BF93FDD00A8377F /* ErrorDetails.swift in Sources */,\n\t\t\t\t0353948B2CA076D000795AA5 /* InboxView+Views.swift in Sources */,\n\t\t\t\t03D662A52F5377630041ADAF /* ActionSeedSections.swift in Sources */,\n\t\t\t\t03BF11CC2D404F2C00CC1F66 /* CommentMaximumDepthSettingsView.swift in Sources */,\n\t\t\t\t0389DDD12C39E1030005B808 /* InstanceListRowBody.swift in Sources */,\n\t\t\t\t0305EBAA2D32B3B80066E5AD /* ModlogView+Logic.swift in Sources */,\n\t\t\t\t033FCAF42C59843E007B7CD1 /* CommunityView.swift in Sources */,\n\t\t\t\tCD7928262C73E73400FA712D /* PersonView+Logic.swift in Sources */,\n\t\t\t\t0382A7F22C0A758E00C79DDA /* ProfileDateView.swift in Sources */,\n\t\t\t\t031E2D5B2BEFC9460003BC45 /* ThemeSettingsView.swift in Sources */,\n\t\t\t\tCDCC7FED2D9AEDC800CE18DA /* BridgeDragValue.swift in Sources */,\n\t\t\t\t03D3A1F12BB9D48E009DE55E /* BasicAction.swift in Sources */,\n\t\t\t\t033F84662D1C780900D87A9E /* ModlogButtonView.swift in Sources */,\n\t\t\t\t03D2A6392C00FAE000ED4FF2 /* UserAccount.swift in Sources */,\n\t\t\t\tCDB738292CB8A6A5005B11BB /* View+PaletteBorder.swift in Sources */,\n\t\t\t\tCD1DF6382D38357500F7851E /* MediaView.swift in Sources */,\n\t\t\t\tCD756A962ED765EC0031D7D1 /* ExportableCommentView.swift in Sources */,\n\t\t\t\t03C93CF02BEFFB1A00327BFE /* LoginCredentialsView.swift in Sources */,\n\t\t\t\tCD13CC652C5D2B9D001AF428 /* CircleCroppedImageView.swift in Sources */,\n\t\t\t\t0325B93E2D3AAE9E00E28B97 /* SettingsHeaderView.swift in Sources */,\n\t\t\t\t0305EBAC2D32C9300066E5AD /* ModlogEntryType+Extensions.swift in Sources */,\n\t\t\t\t03A9FD1C2D7CFD5C007A734D /* Palette+Solarized.swift in Sources */,\n\t\t\t\t03D284022D29E03C00A6659B /* FeedFilterButtonStyle.swift in Sources */,\n\t\t\t\t03AD0A842CFDC557001EF9F7 /* AccountNicknameFieldView.swift in Sources */,\n\t\t\t\t035EDF032C2ED0DE00F51144 /* PersonListRowBody.swift in Sources */,\n\t\t\t\t03FD6CB02C9B719100500FD6 /* View+PopupAnchor.swift in Sources */,\n\t\t\t\t0331715E2CCD6D95002DA370 /* ContentPurgeEditorView.swift in Sources */,\n\t\t\t\tCDBFCB652C03920C008CD468 /* PostLinkHostView.swift in Sources */,\n\t\t\t\t03B04FC02C5FC32300824128 /* SimpleAvatarView.swift in Sources */,\n\t\t\t\t031CA5B72E599F7E00CF0C0F /* View+QuickSwipes.swift in Sources */,\n\t\t\t\t035BE08D2BDE88EC00F77D73 /* NavigationLayerView.swift in Sources */,\n\t\t\t\t035BE0872BDD8DA000F77D73 /* NavigationRootView.swift in Sources */,\n\t\t\t\t03A9FD182D7CFC20007A734D /* Palette+Oled.swift in Sources */,\n\t\t\t\t03A630EF2D497143009A47A6 /* TappableLinksSettingsView.swift in Sources */,\n\t\t\t\t038E62E02F6F0FC600C54DEB /* ContextMenuConfiguration.swift in Sources */,\n\t\t\t\tCDCC7FF12D9B27D800CE18DA /* ZoomRecognizerCoordinator+GestureRecognition.swift in Sources */,\n\t\t\t\t0315B1BE2C74C3D6006D4F82 /* CommentEditorView+Logic.swift in Sources */,\n\t\t\t\tCDB2EC7F2BFADACC00DBC0EF /* HeadlinePostView.swift in Sources */,\n\t\t\t\tCD87BEC32D5132BE0099F190 /* FilterViolationWarning.swift in Sources */,\n\t\t\t\t037352332F27A83900341673 /* PostPollView.swift in Sources */,\n\t\t\t\t0318BA9F2D72405F006CA71F /* PostSortType+Extensions.swift in Sources */,\n\t\t\t\t032C32162C36F65500595286 /* ReplyView.swift in Sources */,\n\t\t\t\tCD1D31832C56D742001B434B /* View+WidthReader.swift in Sources */,\n\t\t\t\t0325B93A2D3A9E8100E28B97 /* InboxBadgeSettingsView.swift in Sources */,\n\t\t\t\tCD7928232C73CBA400FA712D /* TileScoreView.swift in Sources */,\n\t\t\t\t03AF91E12C1B25DE00E56644 /* UIDevice+Extensions.swift in Sources */,\n\t\t\t\t0389DDC52C38917A0005B808 /* InboxItemProviding+Extensions.swift in Sources */,\n\t\t\t\t036ED67B2D0B004D0018E5EA /* AdvancedSortView+SortButton.swift in Sources */,\n\t\t\t\tCD13CC5B2C588B34001AF428 /* WebView.swift in Sources */,\n\t\t\t\t0372EC202D370F0200257095 /* RegistrationApplicationDenialEditorView.swift in Sources */,\n\t\t\t\t039EFEC32BEEBEE0003AC372 /* LoginInstancePickerView.swift in Sources */,\n\t\t\t\t03AB484F2CBAE33500567FF9 /* MarkdownWithLinkList.swift in Sources */,\n\t\t\t\t03531EF12C2DA298004A3464 /* SearchResultsView.swift in Sources */,\n\t\t\t\t033F84512D196AFD00D87A9E /* MessageFeedView+Logic.swift in Sources */,\n\t\t\t\tCD9CFA2A2C2E1E8400739BBC /* FeedHeaderView.swift in Sources */,\n\t\t\t\t03BF11CA2D4027E900CC1F66 /* DevicePickerItem.swift in Sources */,\n\t\t\t\t037029A12D6B9A3900B749DF /* View+TabBarPreview.swift in Sources */,\n\t\t\t\t033F84C12C2AD072002E3EDF /* CommentTreeNode.swift in Sources */,\n\t\t\t\t038E1ACA2F59C18100D30F01 /* CommunitySettingsView.swift in Sources */,\n\t\t\t\t034B94832C09340A00039AF4 /* MarkdownConfiguration+Extensions.swift in Sources */,\n\t\t\t\t034CC0302D22C5BE00C557D3 /* WarningOverlayView.swift in Sources */,\n\t\t\t\tCDB3DDFD2DA485D200F407AB /* SettingsValues.swift in Sources */,\n\t\t\t\t034690932D0F4D720073E664 /* RemovableProviding+Extensions.swift in Sources */,\n\t\t\t\t037F78432D3C129B00D4E180 /* PostThumbnailSettingsView.swift in Sources */,\n\t\t\t\t039F58952C7B618F00C61658 /* InboxSettingsView.swift in Sources */,\n\t\t\t\tCD4D58CF2B86DDEC00B82964 /* AccountSortMode.swift in Sources */,\n\t\t\t\tCDE1F18F2C63D75A008AF042 /* LegacySettings.swift in Sources */,\n\t\t\t\t03D2A6372C00F92400ED4FF2 /* Session.swift in Sources */,\n\t\t\t\t03AF91DD2C1B23E500E56644 /* ImageViewer.swift in Sources */,\n\t\t\t\tCD13CC592C583C7A001AF428 /* WebsitePreviewView.swift in Sources */,\n\t\t\t\tCDA67A9F2D9B32A100E5D17B /* CGPoint+Extensions.swift in Sources */,\n\t\t\t\t03BF11C52D3D3AFB00CC1F66 /* PostReadIndicatorSettingsView.swift in Sources */,\n\t\t\t\tCD869FCE2C15F90C00FC8B5B /* ChildSizeReader.swift in Sources */,\n\t\t\t\t035EDEF12C2DE94B00F51144 /* DefaultTextInputType.swift in Sources */,\n\t\t\t\t039F588C2C7B574E00C61658 /* AdvancedSettingsView.swift in Sources */,\n\t\t\t\tCDDA49A82F7044D2004A5AFF /* InstanceStubResolutionPage.swift in Sources */,\n\t\t\t\t0320B6672C93504600D38548 /* SignUpView+Logic.swift in Sources */,\n\t\t\t\t03E46ACD2D121C19002589DB /* HeadlinePostBodyView.swift in Sources */,\n\t\t\t\t814CEF632F44577A0090F812 /* HiddenReadBannerView.swift in Sources */,\n\t\t\t\t039F58882C7B531800C61658 /* SquircleLabelStyle.swift in Sources */,\n\t\t\t\t035EDEF22C2DE94B00F51144 /* _assignIfNotEqual.swift in Sources */,\n\t\t\t\t035EDEF32C2DE94B00F51144 /* SearchBar.swift in Sources */,\n\t\t\t\t03049A1A2C6502F300FF6889 /* FormSection.swift in Sources */,\n\t\t\t\t035EDEF42C2DE94B00F51144 /* SearchBar+NavigationView.swift in Sources */,\n\t\t\t\t0381F7282F6724D3008A7731 /* ReplyBarConfiguration+Types.swift in Sources */,\n\t\t\t\tCD5581DE2C7B8B820043FAC3 /* ImageFunctions.swift in Sources */,\n\t\t\t\t034065C32D83742900637308 /* View+NavigtionStackPreview.swift in Sources */,\n\t\t\t\t035EDEF52C2DE94B00F51144 /* SearchBarExtensions.swift in Sources */,\n\t\t\t\t03134A522BEAD69F002662CC /* SettingsPage.swift in Sources */,\n\t\t\t\t03049A1C2C65039400FF6889 /* ActiveUserCountView.swift in Sources */,\n\t\t\t\t03A9FD202D7D0072007A734D /* PaletteOption.swift in Sources */,\n\t\t\t\t0389DDDB2C3AB6340005B808 /* ActionBuilder.swift in Sources */,\n\t\t\t\t036ED67F2D0B9A520018E5EA /* CommunitySearchSortPicker.swift in Sources */,\n\t\t\t\t038028FA2CB097CB0091A8A2 /* SearchView+LocationPicker.swift in Sources */,\n\t\t\t\tCDD8B94C2C8234BC00510EBB /* Form.swift in Sources */,\n\t\t\t\t036FFA2F2D45197300998D8A /* PrivacySettingsView.swift in Sources */,\n\t\t\t\t0320B6652C91DBD500D38548 /* NavigationPage+View.swift in Sources */,\n\t\t\t\t03B25B332CC440A600EB6DF5 /* FediseerOpinionView.swift in Sources */,\n\t\t\t\t038C85D82D87696F00543F70 /* FeedToolbarOptions.swift in Sources */,\n\t\t\t\t03F6BD942D500DED006A425E /* PersonMockType.swift in Sources */,\n\t\t\t\t03E46AD42D130728002589DB /* ScoringOperation+Extensions.swift in Sources */,\n\t\t\t\t03CCDAA02BF2795300C0C851 /* LoginPage.swift in Sources */,\n\t\t\t\t032C32082C34469900595286 /* SelectableContentProviding+Extensions.swift in Sources */,\n\t\t\t\t03E46ACB2D1216CE002589DB /* PostViewLinkType.swift in Sources */,\n\t\t\t\t0305EBB22D35C1B70066E5AD /* RegistrationApplicationView.swift in Sources */,\n\t\t\t\t032C32182C36F70300595286 /* ReplyBarConfiguration.swift in Sources */,\n\t\t\t\tCDD4A0A02C8B985D0001AD1A /* ImportExportSettingsView.swift in Sources */,\n\t\t\t\t03B431B62C454D49001A1EB5 /* UIImage+Extensions.swift in Sources */,\n\t\t\t\t03EC86462E9E9D4D004698BB /* PopupAnchorModel.swift in Sources */,\n\t\t\t\tCD93420B2DCD069800945333 /* InstanceUptimeView+Logic.swift in Sources */,\n\t\t\t\tCD1446212A5B328E00610EF1 /* Privacy Policy.swift in Sources */,\n\t\t\t\t03AFD0E32C3C0C540054B8AD /* InstanceView.swift in Sources */,\n\t\t\t\tCDE88C352E68938D00183AE5 /* View+AccountSwitcherGesture.swift in Sources */,\n\t\t\t\tCDD4A09C2C8A122F0001AD1A /* Settings.swift in Sources */,\n\t\t\t\t03AB906F2D418C200054D9E1 /* CommentJumpButtonSettingsView.swift in Sources */,\n\t\t\t\t0381F7162F671427008A7731 /* PostBarConfiguration+Types.swift in Sources */,\n\t\t\t\t033819282D4424D9000AFC55 /* SafetyWarningsSettingsView.swift in Sources */,\n\t\t\t\t030EE3042D651A4100D58C2C /* View+Refreshable.swift in Sources */,\n\t\t\t\t03ECD7212C8654BA00D48BF6 /* PostEditorView+Toolbar.swift in Sources */,\n\t\t\t\tCD3485BD2D501573006748B8 /* ZoomSliderSettingsView.swift in Sources */,\n\t\t\t\t033EF4102CB9AEF7004D8A3F /* ExpandedPostView+Views.swift in Sources */,\n\t\t\t\tCDF60A012E998BB5005FA3F1 /* Data+Extensions.swift in Sources */,\n\t\t\t\t03FE140C2BF953B000A8377F /* HandleError.swift in Sources */,\n\t\t\t\t0320B65A2C8CBB0D00D38548 /* SignUpView+EmailConfirmationView.swift in Sources */,\n\t\t\t\t03ECD71F2C864DB700D48BF6 /* ImageUploadManager.swift in Sources */,\n\t\t\t\tCD1446272A5B36DA00610EF1 /* EULA.swift in Sources */,\n\t\t\t\t036A84D82D99531400E95D50 /* View+ConditionalNavigationTitle.swift in Sources */,\n\t\t\t\tCD79281F2C73B52A00FA712D /* EndOfFeedView.swift in Sources */,\n\t\t\t\tCDE1F19C2C63E2EB008AF042 /* SettingPropertyWrapper.swift in Sources */,\n\t\t\t\t0368F3692D7349D8007DEB70 /* LanguageListRowBody.swift in Sources */,\n\t\t\t\t033F84732D1C784600D87A9E /* ModlogEntryView.swift in Sources */,\n\t\t\t\t033F84742D1C784600D87A9E /* ModlogView.swift in Sources */,\n\t\t\t\t038096612C10AAD8003ED1D8 /* TransitionView.swift in Sources */,\n\t\t\t\t0382A7F42C0A76A900C79DDA /* Date+Extensions.swift in Sources */,\n\t\t\t\tCDBE78C22F38DD8D008B254C /* PersonStubResolutionPage.swift in Sources */,\n\t\t\t\tCD9D243D2CC1DF59006E5F3F /* AccountType.swift in Sources */,\n\t\t\t\t0320B6632C8F8D5A00D38548 /* InstanceSort.swift in Sources */,\n\t\t\t\t0395BCF82D9C57DE00865B33 /* View+Background.swift in Sources */,\n\t\t\t\t0397D4912C6CE871002C6CDC /* PostEditorView.swift in Sources */,\n\t\t\t\t033171782CCE89E3002DA370 /* PurgableProviding+Extensions.swift in Sources */,\n\t\t\t\t03D2A63B2C010B7500ED4FF2 /* GuestAccount.swift in Sources */,\n\t\t\t\t03A818AD2EDCDBA20023E9E8 /* FederationMode+Extensions.swift in Sources */,\n\t\t\t\tCDE1F1962C63DF89008AF042 /* PhoneConstants.swift in Sources */,\n\t\t\t\t031EC5302E5F77D7003408B7 /* FeedContext.swift in Sources */,\n\t\t\t\t0397D4862C6A24D2002C6CDC /* ReportableProviding+Extensions.swift in Sources */,\n\t\t\t\tCD4ED84A2BF1113800EFA0A2 /* View+TabReselectConsumer.swift in Sources */,\n\t\t\t\tCD6DC02C2D86540A00693B16 /* AnimatedAvatarSettingsView.swift in Sources */,\n\t\t\t\tCD4E386D2C836F8C009B24F2 /* UIUserInterfaceStyle+Extensions.swift in Sources */,\n\t\t\t\t03F6BDF82D555F6E006A425E /* ModMailInteractionBarSettingsView.swift in Sources */,\n\t\t\t\tCD4D58A52B86BD1B00B82964 /* PersistenceRepository.swift in Sources */,\n\t\t\t\t037F78412D3C00E600D4E180 /* SettingsInteractionBarSummaryView.swift in Sources */,\n\t\t\t\t035394862C9CDC1100795AA5 /* SubscriptionListNavigationButton.swift in Sources */,\n\t\t\t\t039F58842C7A7F2C00C61658 /* CommentJumpButtonLocation.swift in Sources */,\n\t\t\t\tCDA1D2A52ED8C46B0077A9EA /* ExportableViewComponents.swift in Sources */,\n\t\t\t\tCD1B2E212C7F84160075C7EA /* View+MarkReadOnScroll.swift in Sources */,\n\t\t\t\t033F84AD2C298466002E3EDF /* SectionIndexTitles.swift in Sources */,\n\t\t\t\tCDEE15542D22364B00EB9D7B /* ErrorLogView.swift in Sources */,\n\t\t\t\t03FA318F2C6FEF2000D47FA3 /* InteractionBarActionLabelView.swift in Sources */,\n\t\t\t\tCDE1F1982C63DFC9008AF042 /* PadConstants.swift in Sources */,\n\t\t\t\t03B25B312CC4403500EB6DF5 /* Fediseer.swift in Sources */,\n\t\t\t\t038028F82CB097A10091A8A2 /* SearchView+InstancePicker.swift in Sources */,\n\t\t\t\tCD5C197D2D97096F0089614C /* ZoomCurves.swift in Sources */,\n\t\t\t\t034147FD2D8F5844005503AF /* ExpandedPostHistoryTracker.swift in Sources */,\n\t\t\t\t03EC84422E959AA5004698BB /* PostEditorWebsitePreviewView.swift in Sources */,\n\t\t\t\tCD9CFA2C2C2E1EF300739BBC /* FeedIconView.swift in Sources */,\n\t\t\t\t038188992D43E0F30073E88D /* SafetySettingsView.swift in Sources */,\n\t\t\t\tCD1446252A5B357900610EF1 /* Document.swift in Sources */,\n\t\t\t\t033F84B12C29907F002E3EDF /* FeedbackType.swift in Sources */,\n\t\t\t\t0377BD752DE219A400E38593 /* OnboardingUsernameView.swift in Sources */,\n\t\t\t\tCDB2EC812BFADADF00DBC0EF /* LargePostView.swift in Sources */,\n\t\t\t\tCD03B5BE2F3BA16400AEF786 /* Blockable+Extensions.swift in Sources */,\n\t\t\t\t0389DDC92C39658E0005B808 /* Binding+Extensions.swift in Sources */,\n\t\t\t\t03A9FD1A2D7CFC69007A734D /* Palette+Monochrome.swift in Sources */,\n\t\t\t\t0320B65C2C8CFBDC00D38548 /* ImageUploadHistoryManager.swift in Sources */,\n\t\t\t\tCD7882A72BFFD1A3002E1A30 /* ThumbnailLocation.swift in Sources */,\n\t\t\t\t03036C742C71408700C6DA1D /* CounterAppearance.swift in Sources */,\n\t\t\t\t0397D4722C68078F002C6CDC /* PrefetchingConfiguration+Extensions.swift in Sources */,\n\t\t\t\t0389DDC32C38907C0005B808 /* Message1Providing+Extensions.swift in Sources */,\n\t\t\t\t038E5E892F6D694500C54DEB /* ImageViewerShowControlsSettingsView.swift in Sources */,\n\t\t\t\t03B431B42C4481C3001A1EB5 /* MarkdownTextEditor.swift in Sources */,\n\t\t\t\t0372EBCE2D36FBCF00257095 /* RegistrationApplication+Extensions.swift in Sources */,\n\t\t\t\t038028FD2CB72A2A0091A8A2 /* ContentRemovalEditorView.swift in Sources */,\n\t\t\t\t03B431C42C45BA45001A1EB5 /* AccountPickerMenu.swift in Sources */,\n\t\t\t\t039F58812C7A7E5900C61658 /* JumpButtonView.swift in Sources */,\n\t\t\t\t033F84C82C2B193D002E3EDF /* MlemStats.swift in Sources */,\n\t\t\t\tCD7DB9732C4AEDDE00DCC542 /* TileCommentView.swift in Sources */,\n\t\t\t\tCD4D58AA2B86BE5900B82964 /* AccountListView.swift in Sources */,\n\t\t\t\tCD4D58B92B86D9F800B82964 /* AccountListRow.swift in Sources */,\n\t\t\t\t03B72B6B2C28A0190023A6C4 /* SubscriptionListSettingsView.swift in Sources */,\n\t\t\t\t03F6BD982D500E2A006A425E /* PersonMockType+Realistic.swift in Sources */,\n\t\t\t\tCD27839A2D9B366000DD4C69 /* ZoomableImageView.swift in Sources */,\n\t\t\t\tCD4D58B32B86BFD400B82964 /* AccountsTracker.swift in Sources */,\n\t\t\t\t0381889B2D4412A40073E88D /* SafetyBlurNsfwSettingsView.swift in Sources */,\n\t\t\t\t03ECD71B2C811D6700D48BF6 /* PostEditorView+ImageView.swift in Sources */,\n\t\t\t\t038C85692D861A2100543F70 /* Comment+Mock.swift in Sources */,\n\t\t\t\t0397D48C2C6BE9A2002C6CDC /* CollapsibleSection.swift in Sources */,\n\t\t\t\tCD737FBA2F771BF600E46411 /* InstanceSummarySoftware+Extensions.swift in Sources */,\n\t\t\t\t035DFA232EB3F7240021DE8C /* CommunityAboutView.swift in Sources */,\n\t\t\t\t0311ADB72E4DF49800EC3120 /* SearchHomeView.swift in Sources */,\n\t\t\t\t0377BE9B2DEA328900E38593 /* OnboardingEmailView.swift in Sources */,\n\t\t\t\t038028D82CACAB960091A8A2 /* ModeratorSettingsView.swift in Sources */,\n\t\t\t\t033F84CC2C2B1E46002E3EDF /* HandleThreadiverseLinksModifier.swift in Sources */,\n\t\t\t\t0389DDD32C39E4D40005B808 /* PasteLinkButtonView.swift in Sources */,\n\t\t\t\t03AF91EA2C1CE96600E56644 /* Counter.swift in Sources */,\n\t\t\t\t03D283FC2D25A3F700A6659B /* VisitHistory+CodedData.swift in Sources */,\n\t\t\t\t03B431C22C45BA00001A1EB5 /* MarkdownEditorToolbarView.swift in Sources */,\n\t\t\t\t03AD09E82CF88007001EF9F7 /* MoreRepliesButton.swift in Sources */,\n\t\t\t\t034B948E2C0937BA00039AF4 /* FancyScrollView.swift in Sources */,\n\t\t\t\tCD332D7E2CA7486000A53988 /* String+Extensions.swift in Sources */,\n\t\t\t\t037331A42C9CB12D00C826E1 /* EnvironmentValues+Extensions.swift in Sources */,\n\t\t\t\tCD4D58B52B86BFFB00B82964 /* PersistenceRepository+Dependency.swift in Sources */,\n\t\t\t\t03A630F12D497674009A47A6 /* ShieldsBadgeView.swift in Sources */,\n\t\t\t\tCD4D58EB2B86E63300B82964 /* AssociatedColor.swift in Sources */,\n\t\t\t\tCD4B66DA2DCE809D00D28EB4 /* InstanceUptimeView+Views.swift in Sources */,\n\t\t\t\t0311ADBF2E4F68D000EC3120 /* TopPeopleListView.swift in Sources */,\n\t\t\t\tCD3153072C38421B00BC5FBE /* View+LoadFeed.swift in Sources */,\n\t\t\t\t0302A8802F9BC379000A127A /* SearchHomeCategoryLabelStyle.swift in Sources */,\n\t\t\t\t039F588F2C7B599800C61658 /* ThemeLabel.swift in Sources */,\n\t\t\t\t037029A32D6B9B8400B749DF /* ContentView+Tab.swift in Sources */,\n\t\t\t\tCD2C86542D5556C00034CD8A /* MlemError.swift in Sources */,\n\t\t\t\tCD9857A42C5E7F9D0084C71F /* NsfwOverlayView.swift in Sources */,\n\t\t\t\t030E95E72C80A20A0045BC2C /* View+NavigationTransition.swift in Sources */,\n\t\t\t\tCD45CB0D2D1880E8008BC729 /* FiltersSettingsView.swift in Sources */,\n\t\t\t\t038028FF2CB72AC90091A8A2 /* ReasonShortcutView.swift in Sources */,\n\t\t\t\t03A6316D2D4E3D24009A47A6 /* ModeratorActionSeparationSettingsView.swift in Sources */,\n\t\t\t\t03D2A6422C011F4A00ED4FF2 /* AccountListRowBody.swift in Sources */,\n\t\t\t\t03134A5A2BEC2253002662CC /* AvatarStackView.swift in Sources */,\n\t\t\t\t03AB48572CBC0DFC00567FF9 /* AccountSignInSettingsView.swift in Sources */,\n\t\t\t\t037F78452D3C25AB00D4E180 /* PostSubscriptionIndicatorSettingsView.swift in Sources */,\n\t\t\t\t0397D49C2C6EA73C002C6CDC /* InteractionBarConfiguration.swift in Sources */,\n\t\t\t\t03D3A1F32BB9D49B009DE55E /* ActionGroup.swift in Sources */,\n\t\t\t\t03B7F3352EEEC70F00B00F6A /* NoteEditorView.swift in Sources */,\n\t\t\t\t03049A222C650B2C00FF6889 /* CommunityDetailsView.swift in Sources */,\n\t\t\t\t03F6BD9C2D501478006A425E /* ActorIdentifier+Mock.swift in Sources */,\n\t\t\t\t0381F7242F672258008A7731 /* CommentBarConfiguration+Types.swift in Sources */,\n\t\t\t\tCD7882A92BFFDFC7002E1A30 /* PostTag.swift in Sources */,\n\t\t\t\t0353948F2CA088E600795AA5 /* DeveloperSettingsView.swift in Sources */,\n\t\t\t\tCD7882AB2C013005002E1A30 /* EllipsisMenu.swift in Sources */,\n\t\t\t\tCDBFCB6A2C04EFFE008CD468 /* PostSettingsView.swift in Sources */,\n\t\t\t\t0368F34D2D733215007DEB70 /* SortTimeRange+Extensions.swift in Sources */,\n\t\t\t\t03AB48592CBC14CE00567FF9 /* AccountEmailSettingsView.swift in Sources */,\n\t\t\t\tCD4D58C82B86DCED00B82964 /* AvatarType.swift in Sources */,\n\t\t\t\t03531EEE2C2D9298004A3464 /* SearchSheetView.swift in Sources */,\n\t\t\t\tCDE1F19E2C63E306008AF042 /* Constants.swift in Sources */,\n\t\t\t\t031DBA772F9A93AD00B4BAE4 /* EventRowView.swift in Sources */,\n\t\t\t\tCD4D58BB2B86DA7D00B82964 /* AccountListView+Logic.swift in Sources */,\n\t\t\t\tCD43E8B32BF2C24E007C3D71 /* ContentLoader.swift in Sources */,\n\t\t\t\t031E2D5D2BEFCC630003BC45 /* SettingsView.swift in Sources */,\n\t\t\t\tCDCA44B42C176A4700C092B3 /* Array+Extensions.swift in Sources */,\n\t\t\t\t03CCDAA42BF2852E00C0C851 /* LoginTotpView.swift in Sources */,\n\t\t\t\t033F84782D1D68D200D87A9E /* ModlogEntryContent+Extensions.swift in Sources */,\n\t\t\t\tCD4ED8472BF110FA00EFA0A2 /* TabReselectTracker.swift in Sources */,\n\t\t\t\tCDCC7FF32D9B283A00CE18DA /* ZoomRecognizerCoordinator+Logic.swift in Sources */,\n\t\t\t\t033FCAEE2C57DD14007B7CD1 /* CaptchaDifficulty+Extensions.swift in Sources */,\n\t\t\t\t035BE0912BDEA01E00F77D73 /* View+NavigationSheetModifiers.swift in Sources */,\n\t\t\t\tCDAA02DB2C810DB200D75633 /* Calendar+Extensions.swift in Sources */,\n\t\t\t\tCD5C197F2D970E5F0089614C /* MomentumStatus.swift in Sources */,\n\t\t\t\tCD4D59142B87B36B00B82964 /* InternetConnectionManager.swift in Sources */,\n\t\t\t\t0324FA7B2C1F2CD200F6247D /* InfoStackView.swift in Sources */,\n\t\t\t\tCD7DB9762C4D6C0A00DCC542 /* FeedCommentView.swift in Sources */,\n\t\t\t\tCDCC1BA92D99BEC1006579DF /* ZoomRecognizerCoordinator.swift in Sources */,\n\t\t\t\t03ABE5E42DB8E57000374AFF /* AccountAgeVisibilitySettingsView.swift in Sources */,\n\t\t\t\t03D283FA2D256E1E00A6659B /* VisitHistory.swift in Sources */,\n\t\t\t\t03D3A1EF2BB9CA1D009DE55E /* MenuButton.swift in Sources */,\n\t\t\t\t039F58862C7A810100C61658 /* ExpandedPostView+Logic.swift in Sources */,\n\t\t\t\tCD1C64152D3428710006B3C1 /* CommunityView+Logic.swift in Sources */,\n\t\t\t\t031E2D512BEF961D0003BC45 /* SubscriptionListView.swift in Sources */,\n\t\t\t\tCDD4A09E2C8B69FC0001AD1A /* Button.swift in Sources */,\n\t\t\t\t038028DA2CACACD30091A8A2 /* PostEllipsisMenus.swift in Sources */,\n\t\t\t\t030FF67B2BC8521600F6BFAC /* CustomTabView.swift in Sources */,\n\t\t\t\t03531EF52C2DA610004A3464 /* NavigationSearchType.swift in Sources */,\n\t\t\t\tCDB2EC8A2BFAEFDF00DBC0EF /* ThumbnailImageView.swift in Sources */,\n\t\t\t\t0397D4932C6CE87E002C6CDC /* PostEditorTargetView.swift in Sources */,\n\t\t\t\t034690992D105DFD0073E664 /* InboxView+Types.swift in Sources */,\n\t\t\t\t031DBA792F9A95B200B4BAE4 /* SearchHomeLabelStyle.swift in Sources */,\n\t\t\t\t03F6BDB12D52AA00006A425E /* CommunityMockType+Realistic.swift in Sources */,\n\t\t\t\t0311ADC12E4F693B00EC3120 /* TopInstancesListView.swift in Sources */,\n\t\t\t\t033F84492D18D1F400D87A9E /* MessageFeedView.swift in Sources */,\n\t\t\t\t030050D52D10AE30002B1E99 /* Report+Extensions.swift in Sources */,\n\t\t\t\t0369B3562BFA6824001EFEDF /* InboxView.swift in Sources */,\n\t\t\t\tCD4D59162B87B38C00B82964 /* UIApplication+Extensions.swift in Sources */,\n\t\t\t\t038E5E8B2F6D6C3800C54DEB /* ImageViewerDismissSettingsView.swift in Sources */,\n\t\t\t\tCDB41E8A2C83C24400BD2DE9 /* Section.swift in Sources */,\n\t\t\t\tCD7BF9322D18F4ED0020F2C5 /* FiltersTracker.swift in Sources */,\n\t\t\t\t81A179BC2DDE591700B17017 /* LongPressActionSettingsView.swift in Sources */,\n\t\t\t\tCD44C92C2E5CC8B900F24AC8 /* ConditionalLabelStyleViewModifier.swift in Sources */,\n\t\t\t\t0381F7142F670F95008A7731 /* SwipeActionConfiguration.swift in Sources */,\n\t\t\t\t03B045F62E26D64900540EFB /* SiteSoftwareType+Extensions.swift in Sources */,\n\t\t\t\t0391E0FB2D0066240040CCA8 /* ImageUploadMenu.swift in Sources */,\n\t\t\t\t035BE08B2BDD903100F77D73 /* NavigationModel.swift in Sources */,\n\t\t\t\t033F84BB2C2ACB96002E3EDF /* CommentView.swift in Sources */,\n\t\t\t\tCDEE15522D22190600EB9D7B /* ErrorsTracker.swift in Sources */,\n\t\t\t\t03B62B792CE2A2C00077E9C8 /* RulesPickerView.swift in Sources */,\n\t\t\t\t0389DDD52C39F1290005B808 /* CommunityListRow.swift in Sources */,\n\t\t\t\t038C86572D888EC100543F70 /* CommentSortType+Extensions.swift in Sources */,\n\t\t\t\tCD4BAD352B4B2C0B00A1E726 /* FeedsView.swift in Sources */,\n\t\t\t\t03A630ED2D497005009A47A6 /* ExternalLinkSettingsView.swift in Sources */,\n\t\t\t\t0377BE9F2DEA361600E38593 /* OnboardingModel.swift in Sources */,\n\t\t\t\t0311ADBD2E4F668900EC3120 /* TopCommunitiesListView.swift in Sources */,\n\t\t\t\t03049A1E2C6508F400FF6889 /* RegistrationMode+Extensions.swift in Sources */,\n\t\t\t\t037DE0752CE023E3007F7B92 /* BlockListView.swift in Sources */,\n\t\t\t\tCD3FC6802D4A75090088E63B /* CounterApperance+StaticValues.swift in Sources */,\n\t\t\t\t038028D52CAB479D0091A8A2 /* PostEditorView+Views.swift in Sources */,\n\t\t\t\t0320B64F2C8A638A00D38548 /* SignUpView.swift in Sources */,\n\t\t\t\t036FFA2D2D45110C00998D8A /* ChangePasswordView.swift in Sources */,\n\t\t\t\t03ECD7192C81195000D48BF6 /* PostEditorView+LinkView.swift in Sources */,\n\t\t\t\t030056BB2D7E137800EB0BA3 /* ShareInstancePickerView.swift in Sources */,\n\t\t\t\t0320B6582C8BB3C400D38548 /* SignUpView+Views.swift in Sources */,\n\t\t\t\t03036C832C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift in Sources */,\n\t\t\t\t6DE1183C2A4A217400810C7E /* Profile View.swift in Sources */,\n\t\t\t\t03CBD18D2C6120F600E870BC /* PersonFlair.swift in Sources */,\n\t\t\t\t03EC83252E916C51004698BB /* View+SafeAreaBar.swift in Sources */,\n\t\t\t\t033FCB3E2C5E7FA9007B7CD1 /* View+OutdatedFeedPopup.swift in Sources */,\n\t\t\t\tCD10FA772C7A8622008985AD /* ImageSaver.swift in Sources */,\n\t\t\t\t03267D822BED489C009D6268 /* AvatarBannerView.swift in Sources */,\n\t\t\t\t031CA5752E5900C800CF0C0F /* SwipeConfiguration+Extensions.swift in Sources */,\n\t\t\t\t03D65D702F4B046F0041ADAF /* ContextMenuSettingsView.swift in Sources */,\n\t\t\t\t03500C2D2BF7FC2500CAA076 /* ToastView.swift in Sources */,\n\t\t\t\t0311ADB92E4E0E0800EC3120 /* VisitAgainView.swift in Sources */,\n\t\t\t\tCDB95FCD2EBD3230008669D9 /* SmallOverlayButtonLabel.swift in Sources */,\n\t\t\t\t0316CD642C382A6A009EA8EA /* MessageView.swift in Sources */,\n\t\t\t\tCDF9EF332AB2845C003F885B /* Icons.swift in Sources */,\n\t\t\t\t03AF91E32C1C616F00E56644 /* InteractionBarView.swift in Sources */,\n\t\t\t\t0397D4802C693A88002C6CDC /* [BlockNode]+Extensions.swift in Sources */,\n\t\t\t\t035DF9132EB2AF8E0021DE8C /* GetContentFilter+Extensions.swift in Sources */,\n\t\t\t\t030056A62D7DBD4F00EB0BA3 /* Sharable+Extensions.swift in Sources */,\n\t\t\t\t03DD69422D4FDE8900F8950D /* Person+Mock.swift in Sources */,\n\t\t\t\t03D284062D2AEE3A00A6659B /* TabBarSettingsView.swift in Sources */,\n\t\t\t\t03D006322ECCBA95001BF97D /* QuickSwipeAction+Actions.swift in Sources */,\n\t\t\t\t0320B6612C8DFCF100D38548 /* SearchView+FiltersView.swift in Sources */,\n\t\t\t\t034B94892C09360A00039AF4 /* Int+Extensions.swift in Sources */,\n\t\t\t\t036A84552D98253400E95D50 /* UpdateBannerView.swift in Sources */,\n\t\t\t\t039D75642C4EEE69004F24C2 /* DeletableProviding+Extensions.swift in Sources */,\n\t\t\t\t0391E0FE2D05B2DF0040CCA8 /* AdvancedSortView.swift in Sources */,\n\t\t\t\t031CA0D92E4FBFD800CF0C0F /* MarkAllAsReadButton.swift in Sources */,\n\t\t\t\tCDB2EC882BFAE14800DBC0EF /* FullyQualifiedNameView.swift in Sources */,\n\t\t\t\t031DBA7B2F9A993000B4BAE4 /* SearchHomeListView.swift in Sources */,\n\t\t\t\t03500C272BF69D1D00CAA076 /* ToastModel.swift in Sources */,\n\t\t\t\tCD24CAFC2D5568FE0032B5E8 /* DiscussionLanguageSettingsView.swift in Sources */,\n\t\t\t\t0369B35D2BFB86E3001EFEDF /* Account.swift in Sources */,\n\t\t\t\tCD0C5C102E99629A0074D5A4 /* ExportablePostEditorView.swift in Sources */,\n\t\t\t\t03A814292ED1BCA90023E9E8 /* ModlogView+Filters.swift in Sources */,\n\t\t\t\t036CC3AF2B8145C30098B6A1 /* AppState.swift in Sources */,\n\t\t\t\t033F844D2D18D90900D87A9E /* MessageBubbleView.swift in Sources */,\n\t\t\t\t03134A502BEAD245002662CC /* NavigationLink+NavigationPage.swift in Sources */,\n\t\t\t\t03B431BC2C455838001A1EB5 /* LargePostBodyView.swift in Sources */,\n\t\t\t\tCDFB8C692C7796020070845F /* View+DynamicBlur.swift in Sources */,\n\t\t\t\tCD5CAA0C2D41AE57008E20F2 /* EmbeddingSettingsView.swift in Sources */,\n\t\t\t\t03B25B372CC4478600EB6DF5 /* FediseerInfoView.swift in Sources */,\n\t\t\t\t032A22012EB2A4D100E8B346 /* PersonContentFeedLoader+Extensions.swift in Sources */,\n\t\t\t\t03B62B772CE295530077E9C8 /* RulesListView.swift in Sources */,\n\t\t\t\t03500C2B2BF7F1B100CAA076 /* ToastOverlayView.swift in Sources */,\n\t\t\t\t032C32042C3439C600595286 /* ActorIdentifiable+Extensions.swift in Sources */,\n\t\t\t\tCD57AFA52D0377EB00AB3956 /* AnimationControlLayer.swift in Sources */,\n\t\t\t\tCDFF9A332D88C3C1009E02E2 /* CGFloat+Extensions.swift in Sources */,\n\t\t\t\tCD5D8E2B2D6D5AB100AF5CE4 /* CGSize+Extensions.swift in Sources */,\n\t\t\t\tCD3485BB2D501470006748B8 /* ZoomSliderLocation.swift in Sources */,\n\t\t\t\tCD4D59182B87B3B000B82964 /* UIViewController+Extensions.swift in Sources */,\n\t\t\t\t034B94812C09306D00039AF4 /* CommunityOrPersonStub+Extensions.swift in Sources */,\n\t\t\t\t0320B6692C93506300D38548 /* SearchView+Logic.swift in Sources */,\n\t\t\t\t039F58932C7B616600C61658 /* CommentSettingsView.swift in Sources */,\n\t\t\t\t037DE07A2CE108D9007F7B92 /* FooterLinkView.swift in Sources */,\n\t\t\t\t03D3A1E52BB8B7A3009DE55E /* ActionType.swift in Sources */,\n\t\t\t\t0348F98D2DDBB526006639CD /* OnboardingView.swift in Sources */,\n\t\t\t\t0320B6542C8B65EB00D38548 /* Captcha+Extensions.swift in Sources */,\n\t\t\t\t0397D4A42C6FBC04002C6CDC /* ActionAppearance+StaticValues.swift in Sources */,\n\t\t\t\t03B431C02C45ABFB001A1EB5 /* UITextView+Extensions.swift in Sources */,\n\t\t\t\tCDF8C1912D5D502400295CBA /* InteractionBarWidgetPickerView.swift in Sources */,\n\t\t\t\t034B947F2C091EDD00039AF4 /* ProfileHeaderView.swift in Sources */,\n\t\t\t\t03E0EF432CA73D7A002CB66C /* PostStubResolutionPage.swift in Sources */,\n\t\t\t\tCD9BD5812D8F8F7D0006AB7F /* ZoomRecognizer.swift in Sources */,\n\t\t\t\t030030A12C416B0B009A65FF /* RefreshPopupView.swift in Sources */,\n\t\t\t\t037FC0702E4A6B16009E3E63 /* InstanceView+About.swift in Sources */,\n\t\t\t\tCDBEC30F2E84C9F800F30B00 /* ExportablePostView.swift in Sources */,\n\t\t\t\t0368F3432D72796B007DEB70 /* LanguagePickerSheetView.swift in Sources */,\n\t\t\t\t0397D4A22C6EB035002C6CDC /* ActionAppearance.swift in Sources */,\n\t\t\t\t035394952CA1AE6300795AA5 /* InstanceUptimeView.swift in Sources */,\n\t\t\t\tCDB2EC862BFADD7C00DBC0EF /* FullyQualifiedLabelView.swift in Sources */,\n\t\t\t\tCD7DB9712C49C17200DCC542 /* PersonContentGridView.swift in Sources */,\n\t\t\t\t0368F34F2D734066007DEB70 /* SearchSortType+Extensions.swift in Sources */,\n\t\t\t\t03D2A63D2C010CD400ED4FF2 /* UserSession.swift in Sources */,\n\t\t\t\t03AFD0E12C3B30390054B8AD /* InstanceListRow.swift in Sources */,\n\t\t\t\tCD0E06F72C0E739F00445849 /* PostType+Extensions.swift in Sources */,\n\t\t\t\tCD635E1B2C94DACD00864F75 /* BypassProxyWarningSheet.swift in Sources */,\n\t\t\t\t033F84BD2C2ACC5F002E3EDF /* CommentBarConfiguration.swift in Sources */,\n\t\t\t\t03AF91E52C1C61FA00E56644 /* PostBarConfiguration.swift in Sources */,\n\t\t\t\t03D3A1D42BB88EF1009DE55E /* Action.swift in Sources */,\n\t\t\t\t6363D5C727EE196700E34822 /* ContentView.swift in Sources */,\n\t\t\t\tCD0F280A2C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift in Sources */,\n\t\t\t\t035BE0892BDD901B00F77D73 /* NavigationPage.swift in Sources */,\n\t\t\t\t035BE08F2BDE911900F77D73 /* NavigationLayer.swift in Sources */,\n\t\t\t\t035394992CA1B20B00795AA5 /* InstanceView+Logic.swift in Sources */,\n\t\t\t\tCD8DB93A2D22004000EB0C7B /* Animations.swift in Sources */,\n\t\t\t\t03DD69442D4FEA0000F8950D /* PreviewModifier+SampleEnvironment.swift in Sources */,\n\t\t\t\t03ACE71A2DFD57CC00FD66C0 /* SiteSoftware+Extensions.swift in Sources */,\n\t\t\t\tCD869FCC2C15F8AC00FC8B5B /* BubblePickerView.swift in Sources */,\n\t\t\t\t03267D842BED49CE009D6268 /* AccountSettingsView.swift in Sources */,\n\t\t\t\tCDAA02E12C817AAB00D75633 /* Color+Extensions.swift in Sources */,\n\t\t\t\t03B62C402CE811CB0077E9C8 /* PersonBanEditorView+Logic.swift in Sources */,\n\t\t\t\tCD6DC02A2D86513D00693B16 /* AnimatedAvatarBehavior.swift in Sources */,\n\t\t\t\t03A82FA32C0D1F2400D01A5C /* View+ExternalApiWarning.swift in Sources */,\n\t\t\t\t033F84C32C2B12AA002E3EDF /* InstanceSummary.swift in Sources */,\n\t\t\t\tCD6436502D483C96002668FB /* InteractionBarEditorView+Views.swift in Sources */,\n\t\t\t\t0369B3532BFA514B001EFEDF /* ToastLocation.swift in Sources */,\n\t\t\t\t03F6BD9A2D501041006A425E /* SeededRandomNumberGenerator.swift in Sources */,\n\t\t\t\t036ED6792D0AF3740018E5EA /* PinnedSortTracker.swift in Sources */,\n\t\t\t\tCDC44A362D1CBC280030F01C /* ReadPostIndicator.swift in Sources */,\n\t\t\t\t038E1ABC2F58B9EF00D30F01 /* CommunityActionConfiguration.swift in Sources */,\n\t\t\t\t0353948D2CA080EB00795AA5 /* FeedWelcomeView.swift in Sources */,\n\t\t\t\t038E1AC02F58C76A00D30F01 /* SwipeActionEditorView.swift in Sources */,\n\t\t\t\tCD950E0F2F0ED6F7002A0595 /* FeedPostView.swift in Sources */,\n\t\t\t\t0343C0482D3AD6DB001CF709 /* Set+Extensions.swift in Sources */,\n\t\t\t\t031DBA682F9A65DC00B4BAE4 /* BackendClient+Extensions.swift in Sources */,\n\t\t\t\t034A85712EC0A1FA00E5F904 /* InstanceCommunityListView.swift in Sources */,\n\t\t\t\tCDCC7FEF2D9B1E7100CE18DA /* CachedComputation.swift in Sources */,\n\t\t\t\t03049A202C650A8100FF6889 /* FormReadout.swift in Sources */,\n\t\t\t\t030050D32D109B7E002B1E99 /* ReportView.swift in Sources */,\n\t\t\t\t0397D49A2C6EA6EE002C6CDC /* InteractionBarEditorView.swift in Sources */,\n\t\t\t\t03F6BDAF2D516636006A425E /* CommunityMockType.swift in Sources */,\n\t\t\t\t0300309D2C4163C9009A65FF /* CommentTreeTracker.swift in Sources */,\n\t\t\t\t03A631CC2D4FD18C009A47A6 /* Post+Mock.swift in Sources */,\n\t\t\t\t03FE14082BF94FFB00A8377F /* ErrorView.swift in Sources */,\n\t\t\t\t0325B93C2D3AA62500E28B97 /* InboxItemType+Extensions.swift in Sources */,\n\t\t\t\t030FF67D2BC8524500F6BFAC /* CustomTabItem.swift in Sources */,\n\t\t\t\tCDAA02E32C821C9100D75633 /* Divider.swift in Sources */,\n\t\t\t\t03D284002D26F09500A6659B /* Instance+Extensions.swift in Sources */,\n\t\t\t\t03BF11C72D3D634A00CC1F66 /* AccessibilitySettingsView.swift in Sources */,\n\t\t\t\t033FCAEC2C57DCCD007B7CD1 /* ListingType+Extensions.swift in Sources */,\n\t\t\t\t033FCB272C5E3933007B7CD1 /* AlternateIconLabel.swift in Sources */,\n\t\t\t\t033FCB282C5E3933007B7CD1 /* AlternateIcon.swift in Sources */,\n\t\t\t\t038028F62CB096960091A8A2 /* SearchView+FilterModels.swift in Sources */,\n\t\t\t\t033FCB292C5E3933007B7CD1 /* IconSettingsView.swift in Sources */,\n\t\t\t\tCD03742A2D1DBFD2001E85FA /* ReadCheck.swift in Sources */,\n\t\t\t\t035394932CA1AE2C00795AA5 /* UptimeData.swift in Sources */,\n\t\t\t\t036ED6832D0C483B0018E5EA /* ProfileProviding+Extensions.swift in Sources */,\n\t\t\t\t03EC83F02E9590D3004698BB /* LinkHostView.swift in Sources */,\n\t\t\t\t033FCB2A2C5E3933007B7CD1 /* AlternateIconCell.swift in Sources */,\n\t\t\t\t0389DDC72C389F840005B808 /* UnreadCount+Extensions.swift in Sources */,\n\t\t\t\t03500C242BF55D0E00CAA076 /* Toast.swift in Sources */,\n\t\t\t\t0377BD792DE22D4E00E38593 /* UsernameValidity+Extensions.swift in Sources */,\n\t\t\t\t03F6BDBC2D52B7FE006A425E /* PostMockType.swift in Sources */,\n\t\t\t\t038028D32CAB3D2D0091A8A2 /* ShareActivity.swift in Sources */,\n\t\t\t\tCDCF5A582F1426A5006748E8 /* CommentStubResolutionPage.swift in Sources */,\n\t\t\t\t036ED67D2D0B006C0018E5EA /* TopSortPicker.swift in Sources */,\n\t\t\t\t030FF67F2BC8544700F6BFAC /* CustomTabBarController.swift in Sources */,\n\t\t\t\tCD4D58F82B87B0D100B82964 /* InternetSpeed.swift in Sources */,\n\t\t\t\t0353949C2CA4B3E800795AA5 /* CrossPostListView.swift in Sources */,\n\t\t\t\t0382A7F02C09F0F800C79DDA /* PersonView.swift in Sources */,\n\t\t\t\tCDBFCB6C2C054AA7008CD468 /* TilePostView.swift in Sources */,\n\t\t\t\t0380965F2C10AA80003ED1D8 /* AppState+Transition.swift in Sources */,\n\t\t\t\t03BF11C32D3D135D00CC1F66 /* SearchView+CreatorPicker.swift in Sources */,\n\t\t\t\t031CA4AE2E58A84E00CF0C0F /* View+WithSheetSearch.swift in Sources */,\n\t\t\t\t0315B1C62C754802006D4F82 /* PostEditorView+Logic.swift in Sources */,\n\t\t\t\t0370299F2D6B743B00B749DF /* PostMockType+Realistic.swift in Sources */,\n\t\t\t\tCDE1F1942C63DF44008AF042 /* PlatformConstants.swift in Sources */,\n\t\t\t\t0335AE112D8991330094FFD9 /* View+HiddenNavigationTitle.swift in Sources */,\n\t\t\t\t0397D4602C66113F002C6CDC /* CommentBodyView.swift in Sources */,\n\t\t\t\tCD9CFA372C306DAB00739BBC /* PostGridView.swift in Sources */,\n\t\t\t\t0397D46C2C67E583002C6CDC /* SortingSettingsView.swift in Sources */,\n\t\t\t\t0370299D2D6B70F400B749DF /* MockApiClient+Realistic.swift in Sources */,\n\t\t\t\t03AB48522CBC042E00567FF9 /* AccountContentSettingsView.swift in Sources */,\n\t\t\t\t0377BE072DE645E100E38593 /* OnboardingRecommendInstanceView.swift in Sources */,\n\t\t\t\t03D2A63F2C010DBF00ED4FF2 /* GuestSession.swift in Sources */,\n\t\t\t\tCDD99C3E2C73F4380010367F /* WarningView.swift in Sources */,\n\t\t\t\t030FF6812BC859FD00F6BFAC /* CustomTabViewHostingController.swift in Sources */,\n\t\t\t\t035EDF012C2ECFE000F51144 /* Searchable.swift in Sources */,\n\t\t\t\tCD756A982ED7669C0031D7D1 /* ExportableCommentEditorView.swift in Sources */,\n\t\t\t\tCD7743892C20EDEE0085BB43 /* VotesModel+Extensions.swift in Sources */,\n\t\t\t\tCD4D58AD2B86BE7100B82964 /* QuickSwitcherView.swift in Sources */,\n\t\t\t\t0397D4642C676CA8002C6CDC /* FeedSortPicker.swift in Sources */,\n\t\t\t\t0355F9462C150B2300605248 /* ExternalApiInfoView.swift in Sources */,\n\t\t\t\t03D0273C2CD3BA5100984519 /* PersonContent+Extensions.swift in Sources */,\n\t\t\t\t03A631602D4D1CBB009A47A6 /* HapticSettingsView.swift in Sources */,\n\t\t\t\tCD7C4E572F3B92BA00ADCBDD /* View+ReloadOnAccountSwitch.swift in Sources */,\n\t\t\t\tCD7AC2CC2D7FDFFB00A671B7 /* MediaView+Logic.swift in Sources */,\n\t\t\t\t03F9672B2CE221220081C9A3 /* Label+Profile1.swift in Sources */,\n\t\t\t\t03B25B3B2CC44FFF00EB6DF5 /* UploadConfirmationView.swift in Sources */,\n\t\t\t\tCDD99C3C2C73F3FF0010367F /* DeleteAccountView.swift in Sources */,\n\t\t\t\tCD9CFA302C2E22F600739BBC /* FeedDescription.swift in Sources */,\n\t\t\t\t6363D5C527EE196700E34822 /* MlemApp.swift in Sources */,\n\t\t\t\t03B0EB6F2C87827A00F79FDF /* ExpandedPostView.swift in Sources */,\n\t\t\t\t03D001DD2EC53A97001BF97D /* NavigationPage+PresentationDetents.swift in Sources */,\n\t\t\t\t039F58912C7B5C7A00C61658 /* ContentView+Logic.swift in Sources */,\n\t\t\t\t032C320A2C34495D00595286 /* SelectTextView.swift in Sources */,\n\t\t\t\t0377BE3B2DE8E70D00E38593 /* HapticLevel+Extensions.swift in Sources */,\n\t\t\t\t03E46AD22D130681002589DB /* VotesListView.swift in Sources */,\n\t\t\t\tAD1B0D372A5F7A260006F554 /* Licenses.swift in Sources */,\n\t\t\t\t03F6BDAD2D516615006A425E /* Community+Mock.swift in Sources */,\n\t\t\t\t039F58972C7B68F100C61658 /* AboutMlemView.swift in Sources */,\n\t\t\t\t035EDEFB2C2DF98700F51144 /* CommunityListRowBody.swift in Sources */,\n\t\t\t\t038C85E62D88337100543F70 /* CommentMockType.swift in Sources */,\n\t\t\t\tCD77437F2C1BA5CE0085BB43 /* MultiplatformView.swift in Sources */,\n\t\t\t\t0391E0F92CFF17AE0040CCA8 /* ProfileSettingsView.swift in Sources */,\n\t\t\t\t03E614E72C0BCDC200F692A4 /* FullyQualifiedLinkView.swift in Sources */,\n\t\t\t\tB1B78D642A51D53900F72485 /* AppDelegate.swift in Sources */,\n\t\t\t\t03ABE5B62DB79A0E00374AFF /* DateComponents+Extensions.swift in Sources */,\n\t\t\t\t037F77ED2D3B064B00D4E180 /* SettingsDeviceView.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t6363D5D227EE196A00E34822 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tCD4D583F2B86855F00B82964 /* MlemTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t6363D5DC27EE196A00E34822 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tCD4D58412B86858100B82964 /* MlemUITests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t81DE61BF2F48AF44006E4C36 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t81DE61DE2F48AF4C006E4C36 /* ActionRequestHandler.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t6363D5D827EE196A00E34822 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 6363D5C027EE196700E34822 /* Mlem */;\n\t\t\ttargetProxy = 6363D5D727EE196A00E34822 /* PBXContainerItemProxy */;\n\t\t};\n\t\t6363D5E227EE196A00E34822 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 6363D5C027EE196700E34822 /* Mlem */;\n\t\t\ttargetProxy = 6363D5E127EE196A00E34822 /* PBXContainerItemProxy */;\n\t\t};\n\t\t81DE61CF2F48AF44006E4C36 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 81DE61C22F48AF44006E4C36 /* OpenInMlem */;\n\t\t\ttargetProxy = 81DE61CE2F48AF44006E4C36 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin XCBuildConfiguration section */\n\t\t6363D5E827EE196A00E34822 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++17\";\n\t\t\t\tCLANG_ENABLE_CODE_COVERAGE = NO;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.4;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t6363D5E927EE196A00E34822 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++17\";\n\t\t\t\tCLANG_ENABLE_CODE_COVERAGE = NO;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.4;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"\";\n\t\t\t\t\"SWIFT_OBJC_BRIDGING_HEADER[arch=*]\" = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t6363D5EB27EE196A00E34822 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;\n\t\t\t\tCLANG_ENABLE_CODE_COVERAGE = NO;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Mlem/Mlem.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"local build\";\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"Mlem/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = 8B9GNJW88W;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tEXCLUDED_SOURCE_FILE_NAMES = \"\";\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = Mlem/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = Mlem;\n\t\t\t\tINFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.entertainment\";\n\t\t\t\tINFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = \"\";\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = \"UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 18.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 2.4.1;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.hanners.Mlem;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t6363D5EC27EE196A00E34822 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;\n\t\t\t\tCLANG_ENABLE_CODE_COVERAGE = NO;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Mlem/Mlem.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"local build\";\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"Mlem/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = 8B9GNJW88W;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tEXCLUDED_SOURCE_FILE_NAMES = (\n\t\t\t\t\t\"\\\"$(PROJECT_DIR)/Mlem/App/Utility/Extensions/MlemMiddleware Mock/*\\\"\",\n\t\t\t\t\t\"\\\"$(PROJECT_DIR)/Mlem/Preview Content/PreviewLocalizable.xcstrings\\\"\",\n\t\t\t\t);\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = Mlem/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = Mlem;\n\t\t\t\tINFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.entertainment\";\n\t\t\t\tINFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = \"\";\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = \"UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 18.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 2.4.1;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.hanners.Mlem;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t6363D5EE27EE196A00E34822 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCLANG_ENABLE_CODE_COVERAGE = NO;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = 76ULHQGAPN;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.hanners.MlemTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Mlem.app/Mlem\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t6363D5EF27EE196A00E34822 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCLANG_ENABLE_CODE_COVERAGE = NO;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = 76ULHQGAPN;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.hanners.MlemTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Mlem.app/Mlem\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t6363D5F127EE196A00E34822 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ENABLE_CODE_COVERAGE = NO;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = 76ULHQGAPN;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.hanners.MlemUITests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_TARGET_NAME = Mlem;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t6363D5F227EE196A00E34822 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ENABLE_CODE_COVERAGE = NO;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = 76ULHQGAPN;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.hanners.MlemUITests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_TARGET_NAME = Mlem;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t81DE61D22F48AF44006E4C36 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = ActionIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"local build\";\n\t\t\t\tDEVELOPMENT_TEAM = 8B9GNJW88W;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu17;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = OpenInMlem/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = \"Open in Mlem\";\n\t\t\t\tINFOPLIST_KEY_NSHumanReadableCopyright = \"\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 18.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tLOCALIZATION_PREFERS_STRING_CATALOGS = YES;\n\t\t\t\tMARKETING_VERSION = 2.4.1;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.hanners.Mlem.OpenUrlInMlem;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSTRING_CATALOG_GENERATE_SYMBOLS = YES;\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = \"DEBUG $(inherited)\";\n\t\t\t\tSWIFT_APPROACHABLE_CONCURRENCY = YES;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t81DE61D32F48AF44006E4C36 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = ActionIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"local build\";\n\t\t\t\tDEVELOPMENT_TEAM = 8B9GNJW88W;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu17;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = OpenInMlem/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = \"Open in Mlem\";\n\t\t\t\tINFOPLIST_KEY_NSHumanReadableCopyright = \"\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 18.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tLOCALIZATION_PREFERS_STRING_CATALOGS = YES;\n\t\t\t\tMARKETING_VERSION = 2.4.1;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.hanners.Mlem.OpenUrlInMlem;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSTRING_CATALOG_GENERATE_SYMBOLS = YES;\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_APPROACHABLE_CONCURRENCY = YES;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t6363D5BC27EE196700E34822 /* Build configuration list for PBXProject \"Mlem\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t6363D5E827EE196A00E34822 /* Debug */,\n\t\t\t\t6363D5E927EE196A00E34822 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t6363D5EA27EE196A00E34822 /* Build configuration list for PBXNativeTarget \"Mlem\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t6363D5EB27EE196A00E34822 /* Debug */,\n\t\t\t\t6363D5EC27EE196A00E34822 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t6363D5ED27EE196A00E34822 /* Build configuration list for PBXNativeTarget \"MlemTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t6363D5EE27EE196A00E34822 /* Debug */,\n\t\t\t\t6363D5EF27EE196A00E34822 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t6363D5F027EE196A00E34822 /* Build configuration list for PBXNativeTarget \"MlemUITests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t6363D5F127EE196A00E34822 /* Debug */,\n\t\t\t\t6363D5F227EE196A00E34822 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t81DE61D52F48AF44006E4C36 /* Build configuration list for PBXNativeTarget \"OpenInMlem\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t81DE61D22F48AF44006E4C36 /* Debug */,\n\t\t\t\t81DE61D32F48AF44006E4C36 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\n/* Begin XCRemoteSwiftPackageReference section */\n\t\t037386452BDAFE81007492B5 /* XCRemoteSwiftPackageReference \"LemmyMarkdownUI\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/mlemgroup/LemmyMarkdownUI\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMinorVersion;\n\t\t\t\tminimumVersion = 0.13.0;\n\t\t\t};\n\t\t};\n\t\t0392826E2BC84E480097F91A /* XCRemoteSwiftPackageReference \"SwiftUI-Introspect\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/siteline/SwiftUI-Introspect\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = \"1.4.0-beta.4\";\n\t\t\t};\n\t\t};\n\t\t03EC83EC2E958A44004698BB /* XCRemoteSwiftPackageReference \"OpenGraph\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/satoshi-takano/OpenGraph\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = 1.6.0;\n\t\t\t};\n\t\t};\n\t\t03FA318A2C6FEC5400D47FA3 /* XCRemoteSwiftPackageReference \"SwiftUI-Flow\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/tevelee/SwiftUI-Flow\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = 3.1.1;\n\t\t\t};\n\t\t};\n\t\t50C99B542A61D792005D57DD /* XCRemoteSwiftPackageReference \"swift-dependencies\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/pointfreeco/swift-dependencies\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = 0.5.1;\n\t\t\t};\n\t\t};\n\t\t636250DA2A18111400FC59B4 /* XCRemoteSwiftPackageReference \"KeychainAccess\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/kishikawakatsumi/KeychainAccess.git\";\n\t\t\trequirement = {\n\t\t\t\tbranch = master;\n\t\t\t\tkind = branch;\n\t\t\t};\n\t\t};\n\t\tB104A6D62A59BF3C00B3E725 /* XCRemoteSwiftPackageReference \"Nuke\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/kean/Nuke\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = 12.1.2;\n\t\t\t};\n\t\t};\n\t\tCD4368BF2AE23FD400BD8BD1 /* XCRemoteSwiftPackageReference \"Semaphore\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/groue/Semaphore\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = 0.0.8;\n\t\t\t};\n\t\t};\n\t\tCDE4AC412CA3706F00981010 /* XCRemoteSwiftPackageReference \"SDWebImageWebPCoder\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/SDWebImage/SDWebImageWebPCoder\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = 0.14.6;\n\t\t\t};\n\t\t};\n/* End XCRemoteSwiftPackageReference section */\n\n/* Begin XCSwiftPackageProductDependency section */\n\t\t030FF6782BC84F7E00F6BFAC /* SwiftUIIntrospect */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 0392826E2BC84E480097F91A /* XCRemoteSwiftPackageReference \"SwiftUI-Introspect\" */;\n\t\t\tproductName = SwiftUIIntrospect;\n\t\t};\n\t\t031CA5762E5900E900CF0C0F /* QuickSwipes */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = QuickSwipes;\n\t\t};\n\t\t0341480C2D8F63A6005503AF /* MlemMiddleware */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = MlemMiddleware;\n\t\t};\n\t\t0347A6FA2F97F4CF00EFD670 /* FediverseEvents */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = FediverseEvents;\n\t\t};\n\t\t036879992DA1320000E796EF /* ComponentViews */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = ComponentViews;\n\t\t};\n\t\t037386462BDAFE81007492B5 /* LemmyMarkdownUI */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 037386452BDAFE81007492B5 /* XCRemoteSwiftPackageReference \"LemmyMarkdownUI\" */;\n\t\t\tproductName = LemmyMarkdownUI;\n\t\t};\n\t\t0377BE282DE7A2DE00E38593 /* Haptics */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = Haptics;\n\t\t};\n\t\t0377BF9A2DF0E0E000E38593 /* Rest */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = Rest;\n\t\t};\n\t\t038100502F6AE867008A7731 /* MlemBackend */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = MlemBackend;\n\t\t};\n\t\t03A9FD152D7CEC09007A734D /* Theming */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = Theming;\n\t\t};\n\t\t03D8BF422DA55B6900506687 /* Icons */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = Icons;\n\t\t};\n\t\t03EC83ED2E958A44004698BB /* OpenGraph */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 03EC83EC2E958A44004698BB /* XCRemoteSwiftPackageReference \"OpenGraph\" */;\n\t\t\tproductName = OpenGraph;\n\t\t};\n\t\t03EC85EB2E9D8F37004698BB /* Actions */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = Actions;\n\t\t};\n\t\t03FA318B2C6FECAE00D47FA3 /* Flow */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 03FA318A2C6FEC5400D47FA3 /* XCRemoteSwiftPackageReference \"SwiftUI-Flow\" */;\n\t\t\tproductName = Flow;\n\t\t};\n\t\t50C99B552A61D792005D57DD /* Dependencies */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 50C99B542A61D792005D57DD /* XCRemoteSwiftPackageReference \"swift-dependencies\" */;\n\t\t\tproductName = Dependencies;\n\t\t};\n\t\t636250DB2A18111400FC59B4 /* KeychainAccess */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 636250DA2A18111400FC59B4 /* XCRemoteSwiftPackageReference \"KeychainAccess\" */;\n\t\t\tproductName = KeychainAccess;\n\t\t};\n\t\tB104A6D72A59BF3C00B3E725 /* Nuke */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = B104A6D62A59BF3C00B3E725 /* XCRemoteSwiftPackageReference \"Nuke\" */;\n\t\t\tproductName = Nuke;\n\t\t};\n\t\tB104A6D92A59BF3C00B3E725 /* NukeExtensions */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = B104A6D62A59BF3C00B3E725 /* XCRemoteSwiftPackageReference \"Nuke\" */;\n\t\t\tproductName = NukeExtensions;\n\t\t};\n\t\tB104A6DB2A59BF3C00B3E725 /* NukeUI */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = B104A6D62A59BF3C00B3E725 /* XCRemoteSwiftPackageReference \"Nuke\" */;\n\t\t\tproductName = NukeUI;\n\t\t};\n\t\tB104A6DD2A59BF3C00B3E725 /* NukeVideo */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = B104A6D62A59BF3C00B3E725 /* XCRemoteSwiftPackageReference \"Nuke\" */;\n\t\t\tproductName = NukeVideo;\n\t\t};\n\t\tCD4368C02AE23FD400BD8BD1 /* Semaphore */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CD4368BF2AE23FD400BD8BD1 /* XCRemoteSwiftPackageReference \"Semaphore\" */;\n\t\t\tproductName = Semaphore;\n\t\t};\n\t\tCDA711FA2DB5CAC3008BC3ED /* Media */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = Media;\n\t\t};\n\t\tCDE4AC462CA372B600981010 /* SDWebImageWebPCoder */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CDE4AC412CA3706F00981010 /* XCRemoteSwiftPackageReference \"SDWebImageWebPCoder\" */;\n\t\t\tproductName = SDWebImageWebPCoder;\n\t\t};\n/* End XCSwiftPackageProductDependency section */\n\t};\n\trootObject = 6363D5B927EE196700E34822 /* Project object */;\n}\n"
  },
  {
    "path": "MlemTests/DateTests.swift",
    "content": "//\n// Software Name: Mlem\n// SPDX-FileCopyrightText: Copyright (c) Mlem Group\n// SPDX-License-Identifier: GPL-3.0\n//\n// This software is distributed under the GNU General Public License v3.0 license,\n// the text of which is available at https://www.gnu.org/licenses/gpl-3.0-standalone.html\n// or see the \"LICENSE\" file for more details.\n//\n\nimport SwiftUI\nimport Testing\n\n/// Contains tests cases to check some `Date` extensions utils.\n/// NOTE: Supposed the language of the device / simulator under tests is english\nstruct DateTests {\n    @Test(\n        \"Get relative time must return the localized expected elapsed time for not cake day and more than one year\",\n        .bug(\"https://github.com/mlemgroup/mlem/issues/2032\")\n    )\n    func get_relative_time_returns_localized_string_for_more_than_one_year() {\n        let dateFormatter = DateFormatter()\n        var profileCreationDate: Date, someDate: Date\n\n        // Given\n        dateFormatter.dateFormat = \"yyyy-MM-dd HH:mm:ss Z\"\n        profileCreationDate = dateFormatter.date(from: \"2023-11-14 12:05:00 +0000\")!\n        dateFormatter.dateFormat = \"dd/MM/yyyy\"\n        someDate = dateFormatter.date(from: \"15/05/2025\")!\n        \n        // When\n        let relativeTimeString = profileCreationDate.getRelativeTime(date: someDate, unitsStyle: .full)\n        \n        // Then\n        #expect(relativeTimeString == \"1 year ago\")\n    }\n    \n    @Test(\"Get relative time must return the elapsed days for accounts of several days old\")\n    func get_relative_time_must_return_elapsed_days_for_accounts_of_several_days_old() {\n        let dateFormatter = DateFormatter()\n        var profileCreationDate: Date, profileCreationDateABitLater: Date\n\n        // Given\n        dateFormatter.dateFormat = \"yyyy-MM-dd HH:mm:ss Z\"\n        profileCreationDate = dateFormatter.date(from: \"2025-05-16 12:05:00 +0000\")!\n        profileCreationDateABitLater = dateFormatter.date(from: \"2025-05-20 15:30:22 +0000\")!\n        \n        // When\n        let relativeTimeString = profileCreationDate.getRelativeTime(date: profileCreationDateABitLater, unitsStyle: .full)\n        \n        // Then\n        #expect(relativeTimeString == \"4 days ago\")\n    }\n\n    @Test(\"Get relative time must return the elapsed weeks for accounts of several weeks old\")\n    func get_relative_time_must_return_elapsed_days_for_accounts_of_several_weeks_old() {\n        let dateFormatter = DateFormatter()\n        var profileCreationDate: Date, profileCreationDateABitLater: Date\n\n        // Given\n        dateFormatter.dateFormat = \"yyyy-MM-dd HH:mm:ss Z\"\n        profileCreationDate = dateFormatter.date(from: \"2025-05-01 12:05:00 +0000\")!\n        profileCreationDateABitLater = dateFormatter.date(from: \"2025-05-15 15:30:22 +0000\")!\n        \n        // When\n        let relativeTimeString = profileCreationDate.getRelativeTime(date: profileCreationDateABitLater, unitsStyle: .full)\n        \n        // Then\n        #expect(relativeTimeString == \"2 weeks ago\")\n    }\n\n    @Test(\"Get relative time must return the elapsed months for accounts of several months old\")\n    func get_relative_time_must_return_elapsed_months_for_accounts_of_several_months_old() {\n        let dateFormatter = DateFormatter()\n        var profileCreationDate: Date, profileCreationDateABitLater: Date\n\n        // Given\n        dateFormatter.dateFormat = \"yyyy-MM-dd HH:mm:ss Z\"\n        profileCreationDate = dateFormatter.date(from: \"2025-05-16 12:05:00 +0000\")!\n        profileCreationDateABitLater = dateFormatter.date(from: \"2025-12-16 15:30:22 +0000\")!\n        \n        // When\n        let relativeTimeString = profileCreationDate.getRelativeTime(date: profileCreationDateABitLater, unitsStyle: .full)\n        \n        // Then\n        #expect(relativeTimeString == \"7 months ago\")\n    }\n    \n    @Test(\"Get relative time must return the elapsed hours for accounts younger than one day but older than one hour\")\n    func get_relative_time_must_return_elapsed_hours_for_accounts_of_several_hours_old() {\n        let dateFormatter = DateFormatter()\n        var profileCreationDate: Date, profileCreationDateABitLater: Date\n\n        // Given\n        dateFormatter.dateFormat = \"yyyy-MM-dd HH:mm:ss Z\"\n        profileCreationDate = dateFormatter.date(from: \"2025-05-16 12:05:00 +0000\")!\n        profileCreationDateABitLater = dateFormatter.date(from: \"2025-05-16 15:30:22 +0000\")!\n        \n        // When\n        let relativeTimeString = profileCreationDate.getRelativeTime(date: profileCreationDateABitLater, unitsStyle: .full)\n        \n        // Then\n        #expect(relativeTimeString == \"3 hours ago\")\n    }\n\n    @Test(\"Get relative time must return the elapsed minutes for accounts younger than one hour\")\n    func get_relative_time_must_return_elapsed_hours_for_accounts_of_less_one_hour_old() {\n        let dateFormatter = DateFormatter()\n        var profileCreationDate: Date, profileCreationDateABitLater: Date\n\n        // Given\n        dateFormatter.dateFormat = \"yyyy-MM-dd HH:mm:ss Z\"\n        profileCreationDate = dateFormatter.date(from: \"2025-05-16 12:05:00 +0000\")!\n        profileCreationDateABitLater = dateFormatter.date(from: \"2025-05-16 12:30:22 +0000\")!\n        \n        // When\n        let relativeTimeString = profileCreationDate.getRelativeTime(date: profileCreationDateABitLater, unitsStyle: .full)\n\n        // Then\n        #expect(relativeTimeString == \"25 minutes ago\")\n    }\n\n    @Test(\"Get relative time must return the elapsed seconds for accounts of some seconds old\")\n    func get_relative_time_must_return_elapsed_seconds_for_accounts_of_some_seconds_old() {\n        let dateFormatter = DateFormatter()\n        var profileCreationDate: Date, profileCreationDateABitLater: Date\n\n        // Given\n        dateFormatter.dateFormat = \"yyyy-MM-dd HH:mm:ss Z\"\n        profileCreationDate = dateFormatter.date(from: \"2025-05-16 12:05:00 +0000\")!\n        profileCreationDateABitLater = dateFormatter.date(from: \"2025-05-16 12:05:42 +0000\")!\n        \n        // When\n        let relativeTimeString = profileCreationDate.getRelativeTime(date: profileCreationDateABitLater, unitsStyle: .full)\n\n        // Then\n        #expect(relativeTimeString == \"42 seconds ago\")\n    }\n}\n"
  },
  {
    "path": "MlemTests/MlemTests.swift",
    "content": "//\n//  MlemTests.swift\n//  MlemTests\n//\n//  Created by David Bureš on 25.03.2022.\n//\n\n@testable import Mlem\nimport XCTest\n\nclass MlemTests: XCTestCase {\n    override func setUpWithError() throws {\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n    }\n\n    override func tearDownWithError() throws {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n    }\n\n    func testExample() throws {\n        // This is an example of a functional test case.\n        // Use XCTAssert and related functions to verify your tests produce the correct results.\n        // Any test you write for XCTest can be annotated as throws and async.\n        // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.\n        // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.\n    }\n}\n"
  },
  {
    "path": "MlemUITests/MlemUITests.swift",
    "content": "//\n//  MlemUITests.swift\n//  MlemUITests\n//\n//  Created by David Bureš on 25.03.2022.\n//\n\nimport XCTest\n\nclass MlemUITests: XCTestCase {\n    override func setUpWithError() throws {\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n\n        // In UI tests it is usually best to stop immediately when a failure occurs.\n        continueAfterFailure = false\n\n        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.\n    }\n\n    override func tearDownWithError() throws {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n    }\n\n    func testExample() throws {\n        // UI tests must launch the application that they test.\n        let app = XCUIApplication()\n        app.launch()\n\n        // Use XCTAssert and related functions to verify your tests produce the correct results.\n    }\n\n    func testLaunchPerformance() throws {\n        if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {\n            // This measures how long it takes to launch your application.\n            measure(metrics: [XCTApplicationLaunchMetric()]) {\n                XCUIApplication().launch()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "OpenInMlem/Action.js",
    "content": "//\n//  Action.js\n//  OpenInMlem\n//\n//  Created by Bedir Ekim on 2026-02-20.\n//\n\nvar Action = function() {};\n\nAction.prototype = {\n\n    run: function(arguments) {\n        arguments.completionFunction({ \"url\" : document.URL })\n    },\n\n    finalize: function(arguments) {\n        var deeplink = arguments[\"deeplink\"]\n        if (deeplink) {\n            document.location.href = deeplink\n        }\n    }\n\n};\n\nvar ExtensionPreprocessingJS = new Action\n"
  },
  {
    "path": "OpenInMlem/ActionRequestHandler.swift",
    "content": "//\n//  ActionRequestHandler.swift\n//  OpenInMlem\n//\n//  Created by Bedir Ekim on 2026-02-20.\n//\n\nimport UIKit\nimport MobileCoreServices\nimport UniformTypeIdentifiers\n\nclass ActionRequestHandler: NSObject, NSExtensionRequestHandling {\n\n    var extensionContext: NSExtensionContext?\n\n    func beginRequest(with context: NSExtensionContext) {\n        extensionContext = context\n\n        guard let inputItems = context.inputItems as? [NSExtensionItem] else {\n            done(nil)\n            return\n        }\n\n        for item in inputItems {\n            for provider in item.attachments ?? [] where provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) {\n                provider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil) { item, _ in\n                    guard let dictionary = item as? [String: Any],\n                          let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any],\n                          let urlString = results[\"url\"] as? String,\n                          var components = URLComponents(string: urlString) else {\n                        self.done(nil)\n                        return\n                    }\n                    components.scheme = \"mlem\"\n                    guard let deeplink = components.url?.absoluteString else {\n                        self.done(nil)\n                        return\n                    }\n                    OperationQueue.main.addOperation {\n                        self.done([\"deeplink\": deeplink])\n                    }\n                }\n                return\n            }\n        }\n\n        done(nil)\n    }\n\n    private func done(_ resultsForJS: [String: Any]?) {\n        if let resultsForJS {\n            let dictionary = [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJS]\n            let provider = NSItemProvider(item: dictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier)\n            let item = NSExtensionItem()\n            item.attachments = [provider]\n            extensionContext?.completeRequest(returningItems: [item])\n        } else {\n            extensionContext?.completeRequest(returningItems: [])\n        }\n        extensionContext = nil\n    }\n}\n"
  },
  {
    "path": "OpenInMlem/Info.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>NSExtension</key>\n\t<dict>\n\t\t<key>NSExtensionAttributes</key>\n\t\t<dict>\n\t\t\t<key>NSExtensionActivationRule</key>\n\t\t\t<dict>\n\t\t\t\t<key>NSExtensionActivationSupportsFileWithMaxCount</key>\n\t\t\t\t<integer>0</integer>\n\t\t\t\t<key>NSExtensionActivationSupportsImageWithMaxCount</key>\n\t\t\t\t<integer>0</integer>\n\t\t\t\t<key>NSExtensionActivationSupportsMovieWithMaxCount</key>\n\t\t\t\t<integer>0</integer>\n\t\t\t\t<key>NSExtensionActivationSupportsText</key>\n\t\t\t\t<false/>\n\t\t\t\t<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>\n\t\t\t\t<integer>1</integer>\n\t\t\t</dict>\n\t\t\t<key>NSExtensionJavaScriptPreprocessingFile</key>\n\t\t\t<string>Action</string>\n\t\t\t<key>NSExtensionServiceAllowsFinderPreviewItem</key>\n\t\t\t<true/>\n\t\t\t<key>NSExtensionServiceAllowsTouchBarItem</key>\n\t\t\t<true/>\n\t\t\t<key>NSExtensionServiceFinderPreviewIconName</key>\n\t\t\t<string>NSActionTemplate</string>\n\t\t\t<key>NSExtensionServiceTouchBarBezelColorName</key>\n\t\t\t<string>TouchBarBezel</string>\n\t\t\t<key>NSExtensionServiceTouchBarIconName</key>\n\t\t\t<string>NSActionTemplate</string>\n\t\t</dict>\n\t\t<key>NSExtensionPointIdentifier</key>\n\t\t<string>com.apple.services</string>\n\t\t<key>NSExtensionPrincipalClass</key>\n\t\t<string>$(PRODUCT_MODULE_NAME).ActionRequestHandler</string>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "OpenInMlem/InfoPlist.xcstrings",
    "content": "{\n  \"sourceLanguage\" : \"en\",\n  \"strings\" : {\n    \"CFBundleDisplayName\" : {\n      \"comment\" : \"Bundle display name\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Open in Mlem\"\n          }\n        },\n        \"fr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ouvrir dans Mlem\"\n          }\n        }\n      }\n    },\n    \"CFBundleName\" : {\n      \"comment\" : \"Bundle name\",\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"OpenInMlem\"\n          }\n        }\n      },\n      \"shouldTranslate\" : false\n    },\n    \"NSHumanReadableCopyright\" : {\n      \"comment\" : \"Copyright (human-readable)\",\n      \"extractionState\" : \"extracted_with_value\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"new\",\n            \"value\" : \"\"\n          }\n        }\n      },\n      \"shouldTranslate\" : false\n    }\n  },\n  \"version\" : \"1.0\"\n}"
  },
  {
    "path": "OpenInMlem/Media.xcassets/ActionIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"ActionIcon.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"tinted\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "OpenInMlem/Media.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "PrivacyInfo.xcprivacy",
    "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 <key>NSPrivacyAccessedAPITypes</key>\n <array>\n  <dict>\n   <key>NSPrivacyAccessedAPIType</key>\n   <string>NSPrivacyAccessedAPICategoryUserDefaults</string>\n   <key>NSPrivacyAccessedAPITypeReasons</key>\n   <array>\n    <string>CA92.1</string>\n   </array>\n  </dict>\n </array>\n</dict>\n</plist>"
  },
  {
    "path": "README.md",
    "content": "# [Mlem](https://mlem.group) - A Beautiful iOS Client for Lemmy\n\nMlem is a beautiful, intuitive, open source iOS client for [Lemmy](https://join-lemmy.org) that lets you effortlessly participate in conversations across all Lemmy servers.\n\n<a href=\"https://apps.apple.com/app/id6450543782\"><img src=\"https://mlem.group/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg\" alt=\"Download on the App Store\" width=\"175vw\"></a>\n\n> [!NOTE]\n> Mlem requires iOS 18.0 or later.\n\nIf you'd like to participate in the beta version of Mlem, you can [join our Testflight program](https://testflight.apple.com/join/W6ajfKQt).\n\n## ✨ Why Use Mlem\n\nMlem is built from the ground up to be intuitive and efficient. Its sleek interface lets you fully engage with your favorite communities free of clutter or distraction. Engineered for long-term performance, Mlem won't drain your battery or slow down your device, so you can scroll comfortably all day and night.\n\n## 🚀 Features\n\n| Feeds | Search |\n|:--------:|:--------:|\n| <img src=\"https://mlem.group/screenshots/showcase/feeds.jpeg\" width=\"100%\"> | <img src=\"https://mlem.group/screenshots/showcase/search.jpeg\" width=\"100%\"> |\n\n| Threads | Customize |\n|:--------:|:--------:|\n| <img src=\"https://mlem.group/screenshots/showcase/colored_comment_indents.jpeg\" width=\"100%\"> | <img src=\"https://mlem.group/screenshots/showcase/configurable.jpeg\" width=\"100%\"> |\n\n| Themes | Icons |\n|:--------:|:--------:|\n| <img src=\"https://mlem.group/screenshots/showcase/themes.jpeg\" width=\"100%\"> | <img src=\"https://mlem.group/screenshots/showcase/app_icons.jpeg\" width=\"100%\"> |\n\n## 💬 Want to chat about Mlem?\n\nYou're welcome to join our [community on lemmy.ml](https://lemmy.ml/c/mlemapp) or [Matrix room](https://matrix.to/#/#mlemappspace:matrix.org)!\n\n## 🤝 Contributing\n\nWe welcome contributions from the community! Whether you're interested in fixing bugs or adding new features, your contributions are always welcome and appreciated.\n\nCheck out our [contribution guide](./CONTRIBUTING.md) to get started!\n\n## 📄 License\n\nMlem is fully open source, licensed under GPL 3.0 with an addendum for compliance with the Apple App Store. See [LICENSE](./LICENSE) for details.\n\n### App Icons\nBeehaw Community Icon by Aaron Schneider is included under [CC-BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/).\n"
  },
  {
    "path": "brewfile",
    "content": "brew \"swiftlint\"\nbrew \"swiftformat\"\n"
  }
]