Full Code of zven21/mipha for AI

master 3f3e6171314a cached
319 files
451.9 KB
142.5k tokens
699 symbols
1 requests
Download .txt
Showing preview only (533K chars total). Download the full file or copy to clipboard to get everything.
Repository: zven21/mipha
Branch: master
Commit: 3f3e6171314a
Files: 319
Total size: 451.9 KB

Directory structure:
gitextract_xfkpu7cw/

├── .coveralls.yml
├── .credo.exs
├── .formatter.exs
├── .github/
│   └── workflows/
│       └── elixir.yml
├── .gitignore
├── .iex.exs
├── .travis.yml
├── LICENSE
├── Makefile
├── Procfile
├── README.ZH.md
├── README.md
├── assets/
│   ├── .eslintrc.json
│   ├── .yarnrc
│   ├── css/
│   │   ├── admin.scss
│   │   ├── app/
│   │   │   └── components/
│   │   │       ├── _footer.scss
│   │   │       ├── _notification.scss
│   │   │       ├── base.scss
│   │   │       ├── page.scss
│   │   │       └── user.scss
│   │   ├── app.scss
│   │   └── common/
│   │       ├── components/
│   │       │   └── _header.scss
│   │       └── variables/
│   │           └── mipha.scss
│   ├── js/
│   │   ├── admin.js
│   │   ├── app/
│   │   │   └── components/
│   │   │       ├── editor.js
│   │   │       ├── session.js
│   │   │       ├── times.js
│   │   │       └── topic.js
│   │   ├── app.js
│   │   ├── common/
│   │   │   └── components/
│   │   │       └── utils.js
│   │   └── socket.js
│   ├── package.json
│   ├── static/
│   │   └── robots.txt
│   └── webpack.config.js
├── compile
├── config/
│   ├── config.exs
│   ├── dev.exs
│   ├── prod.exs
│   └── test.exs
├── docker/
│   └── docker-compose.yml
├── elixir_buildpack.config
├── lib/
│   ├── mipha/
│   │   ├── accounts/
│   │   │   ├── accounts.ex
│   │   │   ├── company.ex
│   │   │   ├── location.ex
│   │   │   ├── queries.ex
│   │   │   ├── team.ex
│   │   │   ├── user.ex
│   │   │   └── user_team.ex
│   │   ├── application.ex
│   │   ├── collections/
│   │   │   ├── collection.ex
│   │   │   ├── collections.ex
│   │   │   └── queries.ex
│   │   ├── factory.ex
│   │   ├── follows/
│   │   │   ├── follow.ex
│   │   │   ├── follows.ex
│   │   │   └── queries.ex
│   │   ├── mailer.ex
│   │   ├── markdown/
│   │   │   ├── auto_linker.ex
│   │   │   ├── embed_video_replacer.ex
│   │   │   ├── emoji_replacer.ex
│   │   │   ├── html_renderer.ex
│   │   │   ├── markdown_guides.md
│   │   │   └── mention_replacer.ex
│   │   ├── markdown.ex
│   │   ├── notifications/
│   │   │   ├── notification.ex
│   │   │   ├── notifications.ex
│   │   │   ├── queries.ex
│   │   │   └── user_notification.ex
│   │   ├── qiniu.ex
│   │   ├── regexp.ex
│   │   ├── replies/
│   │   │   ├── queries.ex
│   │   │   ├── replies.ex
│   │   │   └── reply.ex
│   │   ├── repo.ex
│   │   ├── stars/
│   │   │   ├── star.ex
│   │   │   └── stars.ex
│   │   ├── token.ex
│   │   ├── topics/
│   │   │   ├── node.ex
│   │   │   ├── queries.ex
│   │   │   ├── topic.ex
│   │   │   └── topics.ex
│   │   └── utils/
│   │       └── store.ex
│   ├── mipha.ex
│   ├── mipha_web/
│   │   ├── channels/
│   │   │   ├── presence.ex
│   │   │   ├── room_channel.ex
│   │   │   ├── topic_channel.ex
│   │   │   └── user_socket.ex
│   │   ├── controllers/
│   │   │   ├── admin/
│   │   │   │   ├── company_controller.ex
│   │   │   │   ├── node_controller.ex
│   │   │   │   ├── notification_controller.ex
│   │   │   │   ├── page_controller.ex
│   │   │   │   ├── reply_controller.ex
│   │   │   │   ├── team_controller.ex
│   │   │   │   ├── topic_controller.ex
│   │   │   │   └── user_controller.ex
│   │   │   ├── auth_controller.ex
│   │   │   ├── callback_controller.ex
│   │   │   ├── company_controller.ex
│   │   │   ├── location_controller.ex
│   │   │   ├── notification_controller.ex
│   │   │   ├── page_controller.ex
│   │   │   ├── reply_controller.ex
│   │   │   ├── search_controller.ex
│   │   │   ├── session_controller.ex
│   │   │   ├── setting_controller.ex
│   │   │   ├── team_controller.ex
│   │   │   ├── topic_controller.ex
│   │   │   └── user_controller.ex
│   │   ├── email.ex
│   │   ├── endpoint.ex
│   │   ├── gettext.ex
│   │   ├── plugs/
│   │   │   ├── attack.ex
│   │   │   ├── current_user.ex
│   │   │   ├── locale.ex
│   │   │   ├── require_admin.ex
│   │   │   └── require_user.ex
│   │   ├── router.ex
│   │   ├── session.ex
│   │   ├── templates/
│   │   │   ├── admin/
│   │   │   │   ├── company/
│   │   │   │   │   └── index.html.eex
│   │   │   │   ├── node/
│   │   │   │   │   ├── edit.html.eex
│   │   │   │   │   ├── form.html.eex
│   │   │   │   │   ├── index.html.eex
│   │   │   │   │   ├── new.html.eex
│   │   │   │   │   └── show.html.eex
│   │   │   │   ├── notification/
│   │   │   │   │   ├── index.html.eex
│   │   │   │   │   └── show.html.eex
│   │   │   │   ├── page/
│   │   │   │   │   └── index.html.eex
│   │   │   │   ├── reply/
│   │   │   │   │   ├── index.html.eex
│   │   │   │   │   └── show.html.eex
│   │   │   │   ├── team/
│   │   │   │   │   ├── edit.html.eex
│   │   │   │   │   ├── form.html.eex
│   │   │   │   │   ├── index.html.eex
│   │   │   │   │   ├── new.html.eex
│   │   │   │   │   └── show.html.eex
│   │   │   │   ├── topic/
│   │   │   │   │   └── index.html.eex
│   │   │   │   └── user/
│   │   │   │       └── index.html.eex
│   │   │   ├── auth/
│   │   │   │   └── login.html.eex
│   │   │   ├── company/
│   │   │   │   ├── index.html.eex
│   │   │   │   └── show.html.eex
│   │   │   ├── email/
│   │   │   │   ├── forgot_password.html.eex
│   │   │   │   ├── verify_email.html.eex
│   │   │   │   └── welcome.html.eex
│   │   │   ├── layout/
│   │   │   │   ├── _flash.html.eex
│   │   │   │   ├── _footer.html.eex
│   │   │   │   ├── _header.html.eex
│   │   │   │   ├── _navbar.html.eex
│   │   │   │   ├── _sub_header.html.eex
│   │   │   │   ├── admin.html.eex
│   │   │   │   ├── app.html.eex
│   │   │   │   └── email.html.eex
│   │   │   ├── location/
│   │   │   │   ├── index.html.eex
│   │   │   │   └── show.html.eex
│   │   │   ├── notification/
│   │   │   │   ├── _followed.html.eex
│   │   │   │   ├── _reply_comment_added.html.eex
│   │   │   │   ├── _reply_mentioned.html.eex
│   │   │   │   ├── _reply_starred.html.eex
│   │   │   │   ├── _topic_added.html.eex
│   │   │   │   ├── _topic_mentioned.html.eex
│   │   │   │   ├── _topic_reply_added.html.eex
│   │   │   │   ├── _topic_starred.html.eex
│   │   │   │   └── index.html.eex
│   │   │   ├── page/
│   │   │   │   ├── _locations.html.eex
│   │   │   │   ├── index.html.eex
│   │   │   │   └── markdown.html.eex
│   │   │   ├── reply/
│   │   │   │   └── edit.html.eex
│   │   │   ├── search/
│   │   │   │   └── index.html.eex
│   │   │   ├── session/
│   │   │   │   └── new.html.eex
│   │   │   ├── setting/
│   │   │   │   ├── _sidebar.html.eex
│   │   │   │   ├── account.html.eex
│   │   │   │   ├── password.html.eex
│   │   │   │   ├── profile.html.eex
│   │   │   │   ├── reward.html.eex
│   │   │   │   └── show.html.eex
│   │   │   ├── shared/
│   │   │   │   └── _pagination.html.eex
│   │   │   ├── team/
│   │   │   │   ├── _nav.html.eex
│   │   │   │   ├── index.html.eex
│   │   │   │   ├── people.html.eex
│   │   │   │   └── show.html.eex
│   │   │   ├── topic/
│   │   │   │   ├── _editor_toolbar.html.eex
│   │   │   │   ├── _form.html.eex
│   │   │   │   ├── _handle_reply.html.eex
│   │   │   │   ├── _need_register_or_login.html.eex
│   │   │   │   ├── _node_selector.html.eex
│   │   │   │   ├── _nodes.html.eex
│   │   │   │   ├── _operate_toolbar.html.eex
│   │   │   │   ├── _relation_topic.html.eex
│   │   │   │   ├── _replies.html.eex
│   │   │   │   ├── _right.html.eex
│   │   │   │   ├── _right_sidebar.html.eex
│   │   │   │   ├── _topic.html.eex
│   │   │   │   ├── _topics.html.eex
│   │   │   │   ├── edit.html.eex
│   │   │   │   ├── educational.html.eex
│   │   │   │   ├── featured.html.eex
│   │   │   │   ├── index.html.eex
│   │   │   │   ├── jobs.html.eex
│   │   │   │   ├── new.html.eex
│   │   │   │   ├── no_reply.html.eex
│   │   │   │   ├── popular.html.eex
│   │   │   │   └── show.html.eex
│   │   │   └── user/
│   │   │       ├── _left.html.eex
│   │   │       ├── _menu.html.eex
│   │   │       ├── _repos.html.eex
│   │   │       ├── _reward.html.eex
│   │   │       ├── _teams.html.eex
│   │   │       ├── collections.html.eex
│   │   │       ├── followers.html.eex
│   │   │       ├── following.html.eex
│   │   │       ├── forgot_password.html.eex
│   │   │       ├── index.html.eex
│   │   │       ├── invalid_token.html.eex
│   │   │       ├── replies.html.eex
│   │   │       ├── reset_password.html.eex
│   │   │       ├── show.html.eex
│   │   │       ├── topics.html.eex
│   │   │       └── verified.html.eex
│   │   └── views/
│   │       ├── admin/
│   │       │   ├── company_view.ex
│   │       │   ├── node_view.ex
│   │       │   ├── notification_view.ex
│   │       │   ├── page_view.ex
│   │       │   ├── reply_view.ex
│   │       │   ├── team_view.ex
│   │       │   ├── topic_view.ex
│   │       │   └── user_view.ex
│   │       ├── auth_view.ex
│   │       ├── company_view.ex
│   │       ├── email_view.ex
│   │       ├── error_helpers.ex
│   │       ├── error_view.ex
│   │       ├── layout_view.ex
│   │       ├── location_view.ex
│   │       ├── notification_view.ex
│   │       ├── page_view.ex
│   │       ├── reply_view.ex
│   │       ├── search_view.ex
│   │       ├── session_view.ex
│   │       ├── setting_view.ex
│   │       ├── shared_view.ex
│   │       ├── tag_helpers.ex
│   │       ├── team_view.ex
│   │       ├── topic_view.ex
│   │       ├── user_view.ex
│   │       └── view_helpers.ex
│   └── mipha_web.ex
├── mix.exs
├── phoenix_static_buildpack.config
├── priv/
│   ├── gettext/
│   │   ├── default.pot
│   │   ├── en/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── default.po
│   │   │       └── errors.po
│   │   ├── errors.pot
│   │   └── zh/
│   │       └── LC_MESSAGES/
│   │           ├── default.po
│   │           └── errors.po
│   └── repo/
│       ├── migrations/
│       │   ├── 20180629095811_create_users.exs
│       │   ├── 20180702075249_create_topics.exs
│       │   ├── 20180702081217_create_replies.exs
│       │   ├── 20180702081845_create_nodes.exs
│       │   ├── 20180703094024_create_locations.exs
│       │   ├── 20180704024042_create_collections.exs
│       │   ├── 20180704024600_create_follows.exs
│       │   ├── 20180704025308_create_stars.exs
│       │   ├── 20180704032212_create_companies.exs
│       │   ├── 20180704053120_create_teams.exs
│       │   ├── 20180704053159_create_users_teams.exs
│       │   ├── 20180712015550_create_notifications.exs
│       │   └── 20180712015614_create_users_notifications.exs
│       └── seeds.exs
├── script/
│   ├── credo
│   ├── iex
│   ├── serve
│   └── setup
└── test/
    ├── mipha/
    │   ├── accounts/
    │   │   ├── accounts_test.exs
    │   │   ├── company_test.exs
    │   │   ├── location_test.exs
    │   │   ├── queries_test.exs
    │   │   └── user_test.exs
    │   ├── collections/
    │   │   ├── collection_test.exs
    │   │   ├── collections_test.exs
    │   │   └── queries_test.exs
    │   ├── follows/
    │   │   ├── follow_test.exs
    │   │   ├── follows_test.exs
    │   │   └── queries_test.exs
    │   ├── notifications/
    │   │   ├── notification_test.exs
    │   │   ├── notifications_test.exs
    │   │   ├── queries_test.exs
    │   │   └── user_notification_test.exs
    │   ├── replies/
    │   │   ├── queries_test.exs
    │   │   ├── replies_test.exs
    │   │   └── reply_test.exs
    │   ├── stars/
    │   │   ├── star_test.exs
    │   │   └── stars_test.exs
    │   └── topics/
    │       ├── node_test.exs
    │       ├── queries_test.exs
    │       ├── topic_test.exs
    │       └── topics_test.exs
    ├── mipha_web/
    │   ├── channels/
    │   │   ├── room_channel_test.exs
    │   │   └── topic_channel_test.exs
    │   ├── controllers/
    │   │   ├── admin/
    │   │   │   ├── company_controller_test.exs
    │   │   │   ├── node_controller_test.exs
    │   │   │   ├── page_controller_test.exs
    │   │   │   ├── reply_controller_test.exs
    │   │   │   ├── team_controller_test.exs
    │   │   │   ├── topic_controller_test.exs
    │   │   │   └── user_controller_test.exs
    │   │   ├── auth_controller_test.exs
    │   │   ├── location_controller_test.exs
    │   │   ├── notification_controller_test.exs
    │   │   ├── page_controller_test.exs
    │   │   ├── reply_controller_test.exs
    │   │   ├── session_controller_test.exs
    │   │   ├── setting_controller_test.exs
    │   │   ├── topic_controller_test.exs
    │   │   └── user_controller_test.exs
    │   └── views/
    │       ├── error_view_test.exs
    │       ├── layout_view_test.exs
    │       └── page_view_test.exs
    ├── support/
    │   ├── channel_case.ex
    │   ├── conn_case.ex
    │   └── data_case.ex
    └── test_helper.exs

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveralls.yml
================================================
service_name: travis-pro
repo_token: 2ZS2MYr7uA24NM8smvOIExISDZpNgS5VN

================================================
FILE: .credo.exs
================================================
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
  #
  # You can have as many configs as you like in the `configs:` field.
  configs: [
    %{
      #
      # Run any exec using `mix credo -C <name>`. If no exec name is given
      # "default" is used.
      #
      name: "default",
      #
      # These are the files included in the analysis:
      files: %{
        #
        # You can give explicit globs or simply directories.
        # In the latter case `**/*.{ex,exs}` will be used.
        #
        included: ["lib/", "src/", "test/", "web/", "apps/"],
        excluded: [~r"/_build/", ~r"/deps/"]
      },
      #
      # If you create your own checks, you must specify the source files for
      # them here, so they can be loaded by Credo before running the analysis.
      #
      requires: [],
      #
      # If you want to enforce a style guide and need a more traditional linting
      # experience, you can change `strict` to `true` below:
      #
      strict: false,
      #
      # If you want to use uncolored output by default, you can change `color`
      # to `false` below:
      #
      color: true,
      #
      # You can customize the parameters of any check by adding a second element
      # to the tuple.
      #
      # To disable a check put `false` as second element:
      #
      #     {Credo.Check.Design.DuplicatedCode, false}
      #
      checks: [
        #
        ## Consistency Checks
        #
        {Credo.Check.Consistency.ExceptionNames},
        {Credo.Check.Consistency.LineEndings},
        {Credo.Check.Consistency.ParameterPatternMatching},
        {Credo.Check.Consistency.SpaceAroundOperators},
        {Credo.Check.Consistency.SpaceInParentheses},
        {Credo.Check.Consistency.TabsOrSpaces},

        #
        ## Design Checks
        #
        # You can customize the priority of any check
        # Priority values are: `low, normal, high, higher`
        #
        {Credo.Check.Design.AliasUsage, false},
        # For some checks, you can also set other parameters
        #
        # If you don't want the `setup` and `test` macro calls in ExUnit tests
        # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just
        # set the `excluded_macros` parameter to `[:schema, :setup, :test]`.
        #
        {Credo.Check.Design.DuplicatedCode, excluded_macros: []},
        # You can also customize the exit_status of each check.
        # If you don't want TODO comments to cause `mix credo` to fail, just
        # set this value to 0 (zero).
        #
        {Credo.Check.Design.TagTODO, false},
        {Credo.Check.Design.TagFIXME, false},

        #
        ## Readability Checks
        #
        {Credo.Check.Readability.AliasOrder, false},
        {Credo.Check.Readability.FunctionNames},
        {Credo.Check.Readability.LargeNumbers},
        {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 180},
        {Credo.Check.Readability.ModuleAttributeNames},
        {Credo.Check.Readability.ModuleDoc},
        {Credo.Check.Readability.ModuleNames},
        {Credo.Check.Readability.ParenthesesOnZeroArityDefs},
        {Credo.Check.Readability.ParenthesesInCondition},
        {Credo.Check.Readability.PredicateFunctionNames},
        {Credo.Check.Readability.PreferImplicitTry},
        {Credo.Check.Readability.RedundantBlankLines},
        {Credo.Check.Readability.StringSigils},
        {Credo.Check.Readability.TrailingBlankLine},
        {Credo.Check.Readability.TrailingWhiteSpace},
        {Credo.Check.Readability.VariableNames},
        {Credo.Check.Readability.Semicolons},
        {Credo.Check.Readability.SpaceAfterCommas},

        #
        ## Refactoring Opportunities
        #
        {Credo.Check.Refactor.DoubleBooleanNegation, false},
        {Credo.Check.Refactor.CondStatements},
        {Credo.Check.Refactor.CyclomaticComplexity},
        {Credo.Check.Refactor.FunctionArity},
        {Credo.Check.Refactor.LongQuoteBlocks},
        {Credo.Check.Refactor.MatchInCondition},
        {Credo.Check.Refactor.NegatedConditionsInUnless},
        {Credo.Check.Refactor.NegatedConditionsWithElse},
        {Credo.Check.Refactor.Nesting},
        {Credo.Check.Refactor.PipeChainStart,
         excluded_argument_types: [:atom, :binary, :fn, :keyword], excluded_functions: []},
        {Credo.Check.Refactor.UnlessWithElse},

        #
        ## Warnings
        #
        {Credo.Check.Warning.BoolOperationOnSameValues},
        {Credo.Check.Warning.ExpensiveEmptyEnumCheck},
        {Credo.Check.Warning.IExPry},
        {Credo.Check.Warning.IoInspect},
        {Credo.Check.Warning.LazyLogging},
        {Credo.Check.Warning.OperationOnSameValues},
        {Credo.Check.Warning.OperationWithConstantResult},
        {Credo.Check.Warning.UnusedEnumOperation},
        {Credo.Check.Warning.UnusedFileOperation},
        {Credo.Check.Warning.UnusedKeywordOperation},
        {Credo.Check.Warning.UnusedListOperation},
        {Credo.Check.Warning.UnusedPathOperation},
        {Credo.Check.Warning.UnusedRegexOperation},
        {Credo.Check.Warning.UnusedStringOperation},
        {Credo.Check.Warning.UnusedTupleOperation},
        {Credo.Check.Warning.RaiseInsideRescue},

        #
        # Controversial and experimental checks (opt-in, just remove `, false`)
        #
        {Credo.Check.Refactor.ABCSize, false},
        {Credo.Check.Refactor.AppendSingleItem, false},
        {Credo.Check.Refactor.VariableRebinding, false},
        {Credo.Check.Warning.MapGetUnsafePass, false},
        {Credo.Check.Consistency.MultiAliasImportRequireUse, false},

        #
        # Deprecated checks (these will be deleted after a grace period)
        #
        {Credo.Check.Readability.Specs, false}

        #
        # Custom checks can be created using `mix credo.gen.check`.
        #
      ]
    }
  ]
}


================================================
FILE: .formatter.exs
================================================
[
  inputs: [
    "{lib,test,priv}/**/*.{ex,exs}",
    "mix.exs",
    ".formatter.exs",
    ".iex.exs",
    ".credo.exs"
  ],
  import_deps: [:ecto, :plug, :phoenix]
]


================================================
FILE: .github/workflows/elixir.yml
================================================
name: Elixir CI

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres
        ports:
          - 5432:5432
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_USER: postgres
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
    - uses: actions/checkout@v2
    - name: Setup elixir
      uses: actions/setup-elixir@v1
      with:
        elixir-version: 1.9.4 # Define the elixir version [required]
        otp-version: 22.2 # Define the OTP version [required]
    - name: Install Dependencies
      run: mix deps.get
    - name: Run Tests
      run: mix test
      env: 
        DB_HOST: postgres


================================================
FILE: .gitignore
================================================
# App artifacts
/_build
/db
/deps
/*.ez

# Generated on crash by the VM
erl_crash.dump

# Generated on crash by NPM
npm-debug.log

# Static artifacts
/assets/node_modules

# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/

# Files matching config/*.secret.exs pattern contain sensitive
# data and you should not commit them into version control.
#
# Alternatively, you may comment the line below and commit the
# secrets files as long as you replace their contents by environment
# variables.
/config/*.secret.exs
/assets/yarn.lock
/assets/package-lock.json

# Ingore the whole configuration files from the .idea folder
/.idea
/.elixir_ls

================================================
FILE: .iex.exs
================================================
import Ecto.{Changeset, Query}

alias Mipha.{
  Repo,
  Accounts,
  Topics,
  Replies,
  Stars,
  Collections,
  Follows,
  Markdown,
  Mailer,
  Notifications,
  Utils,
  Token
}

alias Accounts.{User, Location, Company, Team}
alias Topics.{Node, Topic}
alias Replies.Reply
alias Stars.Star
alias Collections.Collection
alias Follows.Follow
alias Notifications.{Notification, UserNotification}
alias Utils.Store


================================================
FILE: .travis.yml
================================================
language: elixir
elixir:
  - '1.8.1'
addons:
  postgresql: '9.4'
services:
  - postgresql
cache:
  directories:
    - _build
    - deps
env:
  - MIX_ENV=test
before_script:
  - mix ecto.create && mix ecto.migrate
script:
  - mix test
  - mix coveralls.travis


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2018 Zven Wang

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:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

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.


================================================
FILE: Makefile
================================================
# code helper
iex:
	iex --erl "-kernel shell_history enabled" -S mix
serve:
	iex --erl "-kernel shell_history enabled" -S mix phx.server
credo:
	mix credo --strict
coveralls:
	mix coveralls

# docker helper
start.pg:
	docker-compose -f docker/docker-compose.yml up -d

================================================
FILE: Procfile
================================================
web: MIX_ENV=prod mix phx.server


================================================
FILE: README.ZH.md
================================================
# Mipha

[![Build Status](https://travis-ci.org/zven21/mipha.svg?branch=master)](https://travis-ci.org/zven21/mipha)
[![Coverage Status](https://coveralls.io/repos/github/zven21/mipha/badge.svg?branch=excoveralls)](https://coveralls.io/github/zven21/mipha?branch=excoveralls)

[English](./README.md) | 简体中文

## 目录

* [简介](#简介)
* [启动程序](#启动程序)
* [数据库表关系](#数据库表关系)
* [项目结构](#项目结构)
* [进度与计划](#进度与计划)
* [其他](#其他)

## 简介

Mipha 是一个用 Elixir 模(chao)仿(xi) [RubyChina](https://ruby-china.org/) 的开源论坛。
有兴趣参与开发的童鞋,可以加入 [Slack channel](https://elixir-mipha.slack.com/)

## 启动程序

```bash
# clone 项目
git clone git@github.com:zven21/mipha.git
# 初始化,如果是 Mac 电脑,可以执行 ./script/setup,
cd mipha && ./script/setup
# 数据库初始化,在 config/dev.exs 内配置开发环境的 postgres 账号和密码
mix ecto.reset
# 启动项目 :-)
mix phx.serve
```

如果要使用上传图片、发送邮件或第三方 Github 登录功能,需要配置环境变量配置在  `~/.profile` 或 `~/.zshrc` 中

```bash
# 七牛图片服务器的账号和密码
export QINIU_ACCESS_KEY
export QINIU_SECRET_KEY
# 发送邮件服务器的账号和密码
export GMAIL_USERNAME
export GMAIL_PASSWORD
# Github 第三方认证登录的 Key 与 Secret
export GITHUB_CLIENT_ID
export GITHUB_CLIENT_SECRET
```

## 数据库表关系

![ ](https://l.ruby-china.com/photo/2018/b96739ac-94d4-433e-9693-de528466c6d3.jpeg!large)

## 项目结构

目前的项目结构设计很直接,不属于 web 部分的功能,都放到了 lib/mipha 内,后续会根据业务需求调整。

```bash
.
├── assets                     # JS CSS 与静态资源
├── lib                        # 项目 elixir 代码
│   ├── mipha                  # 目前的逻辑是非 Web 部分的代码放到这里。
│   │   ├── accounts           # 社区用户、团队、公司、地址
│   │   ├── collections        # 收藏
│   │   ├── follows            # 关注
│   │   ├── markdown           # markdown 文本解析相关策略
│   │   ├── notifications      # 站内信(通知)
│   │   ├── replies            # 帖子评论
│   │   ├── stars              # 点赞,目前支持点赞帖子与评论
│   │   ├── topics             # 帖子与帖子的分类(节点)
│   │   ├── utils              # 工具库
│   │   ├── mailer.ex          # 发送邮件
│   │   ├── markdown.ex        # markdown 文本解析
│   │   ├── qiniu.ex           # 七牛上传图片
│   │   ├── regexp.ex          # 正则表达式
│   │   ├── token.ex           # token 验证
│   ├── mipha_web
│   │   ├── channels           # socket WS 协议相关代码
│   │   ├── controllers        # Controllers
│   │   │   ├── admin          # admin 管理台
│   │   ├── plugs              # Plugs
│   │   ├── templates          # Templates
│   │   ├── views              # Views
│   │   ├── email.ex           # 发送邮件方法及调用邮件模板
│   │   ├── session.ex         # 用户登录相关的 session 处理
│   ├── mipha.ex
│   ├── mipha_web.ex
└── test                       # 测试

```

## 进度与计划

目前在[第一迭代](https://github.com/zven21/mipha/milestone/1),按照 RubyChina 的功能实现。欢迎提 Issue 或 PR。

## 其他

* [[开源项目] 用 Elixir 撸了一个 RubyChina](https://ruby-china.org/topics/37158)


================================================
FILE: README.md
================================================
# Mipha

[![Build Status](https://travis-ci.org/zven21/mipha.svg?branch=master)](https://travis-ci.org/zven21/mipha)
[![Coverage Status](https://coveralls.io/repos/github/zven21/mipha/badge.svg?branch=excoveralls)](https://coveralls.io/github/zven21/mipha?branch=excoveralls)


English | [简体中文](./README.ZH.md)

## Table of contents

* [Introduction](#introduction)
* [Getting started](#getting-started)
* [Database relationship](#database-relationship)
* [Project structure](#project-structure)
* [Make a pull request](#make-a-pull-request)
* [License](#license)

## Introduction

Mipha is an open-source elixir forum build with phoenix 1.4 (inspire by [homeland](https://ruby-china.org)).

## Getting started

```bash
# clone
git clone git@github.com:zven21/mipha.git
# init setup
cd mipha && ./script/setup
# db create && db migrate db seeds
mix ecto.reset
# run
mix phx.serve
```

## Database relationship

![ ](https://l.ruby-china.com/photo/2018/b96739ac-94d4-433e-9693-de528466c6d3.jpeg!large)

## Project structure

```bash
.
├── assets                     # JS CSS and static file.
├── lib                        #
│   ├── mipha                  #
│   │   ├── accounts           # user team company location model.
│   │   ├── collections        # user collection.
│   │   ├── follows            # follows
│   │   ├── markdown           # markdown
│   │   ├── notifications      # notification
│   │   ├── replies            # the reply of topic.
│   │   ├── stars              # like topic or reply
│   │   ├── topics             # topic and node.
│   │   ├── utils              #
│   │   ├── mailer.ex          # send email.
│   │   ├── markdown.ex        #
│   │   ├── qiniu.ex           # image upload
│   │   ├── regexp.ex          # regex
│   │   ├── token.ex           # token verification, reset password etc.
│   ├── mipha_web
│   │   ├── channels           # socket
│   │   ├── controllers        # Controllers
│   │   │   ├── admin          # admin dashboard
│   │   ├── plugs              # Plugs
│   │   ├── templates          # Templates
│   │   ├── views              # Views
│   │   ├── email.ex           #
│   │   ├── session.ex         #
│   ├── mipha.ex
│   ├── mipha_web.ex
└── test                       # test
```

## Contributing

Bug report or pull request are welcome.

## Make a pull request

1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request

Please write unit test with your code if necessary.

## License

The proj is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

================================================
FILE: assets/.eslintrc.json
================================================
{
  "parser": "babel-eslint",
  "extends": ["prettier"],
  "rules": {
    "indent": 0
  }
}


================================================
FILE: assets/.yarnrc
================================================
--ignore-engines true

================================================
FILE: assets/css/admin.scss
================================================
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome.scss";
@import "~bootstrap/scss/bootstrap";
@import "./common/variables/mipha";
@import "./common/components/header";

body {
  background-color: #e5e5e5;
}

@media (min-width: 1200px) {
  .container {
    max-width: 1480px;
  }
}

.card {
  margin-top: 10px;
  margin-bottom: 5px;
}

body,
p,
ol,
ul,
td,
a,
span {
  font-size: 14px;
  line-height: 18px;
}

body {
  color: #333;
}

body,
p,
ol,
ul,
td {
  font-family: verdana, arial, helvetica, sans-serif;
  font-size: 13px;
  line-height: 18px;
}

.header {
  .navbar-nav-scroll {
    .navbar-nav {
      width: 100%;
      height: 50px;
      overflow-x: scroll;
    }
  }
  #navbar-new-menu,
  .notification-count {
    display: none;
  }
  .user-bar {
    background: #FFF;
    min-width: auto;
  }
}

.nav-item .nav-link {
  font-size: 14px;
  padding-left: 10px !important;
  padding-right: 10px !important;
  &.active {
    color: $grape;
  }
}

.navbar-search {
  display: none !important;
}

.card-body h1,
fieldset legend {
  border: 0px;
  font-size: 18px;
  margin-top: 0px;
  margin-bottom: 20px;
  padding: 0;
  line-height: 100%;
}

pre {
  background-color: #eee;
  padding: 10px;
  font-size: 11px;
}

.group {
  margin-bottom: 20px;
}

.group h2 {
  font-size: 12px;
  margin-bottom: 8px;
}

.group ul {
  margin: 0 20px;
}

.pagination {
  clear: both;
  margin-top: 10px;
  text-align: left;
}

.toolbar {
  margin-bottom: 15px;
}

table tr.deleted td {
  text-decoration: line-through;
  color: #999
}

table tr td a.fa {
  text-decoration: normal;
  color: #666;
  margin-right: 4px;
  &:hover {
    color: #333;
    text-decoration: none;
  }
}

.nav>li>a {
  padding: 14px 6px;
}

.stat {
  margin: 15px;
  padding-bottom: 15px;
  border-bottom: 1px dashed #eee;
  .total {
    font-size: 18px;
    color: #555;
    margin-bottom: 10px;
    margin-top: 8px;
  }
  .name {
    font-size: 14px;
    color: #999;
  }
  .total-week {
    color: $grape;
    font-size: 12px;
    margin: 0 10px;
  }
  .total-month {
    color: $green;
    font-size: 12px;
  }
}

================================================
FILE: assets/css/app/components/_footer.scss
================================================
footer {
  margin-top: 10px;
  margin-bottom: 20px;
  color: #909090;
  a {
    color: #666;
  }
  .links {
    color: #ddd;
  }
  .socials {
    a {
      font-size: 20px;
      margin-right: 8px;
    }
  }
}

================================================
FILE: assets/css/app/components/_notification.scss
================================================
.notifications {
  .card-header {
    font-size: 16px;
    line-height: 32px;
  }
  .card-body {
    padding-top: 0;
    padding-bottom: 0;
  }
  .notification-group {
    padding: 10px 0;
    .group-title {
      color: #aaa;
      border-bottom: 1px solid #eee;
      padding: 5px 0;
    }
  }
  .notification {
    margin: 0 -15px;
    padding: 10px 15px;
    &:last-child {
      border-bottom: 0;
    }
    a {
      color: #555;
    }
    &.unread {
      color: #444;
      .media-heading {
        font-weight: bold;
      }
      a {
        color: #222;
        text-decoration: underline;
      }
    }
    .media-content {
      color: #444;
      a {
        color: #999 !important;
      }
      p {
        font-size: 14px;
        margin-bottom: 6px;
      }
      p:last-child {
        margin-bottom: 0;
      }
    }
    .user-avatar {
      img {
        width: 32px;
        height: 32px;
        border-radius: 120px;
      }
    }
    .media-right {
      min-width: 40px;
      color: #AAA;
      font-size: 13px;
    }
  }
}

================================================
FILE: assets/css/app/components/base.scss
================================================
@mixin clearfix() {
  &:before,
  &:after {
    content: " ";
    display: table;
  }
  &:after {
    clear: both;
  }
  & {
    zoom: 1;
  }
}

/* 去掉 Bootstrap 的蓝色 outline */

* {
  outline-style: none;
}

textarea,
input[text] {
  outline: none !important;
  box-shadow: none !important;
  -webkit-appearance: none !important;
}

textarea,
#preview {
  overflow: scroll;
  &::-webkit-scrollbar {
    width: 12px;
  }
  &::-webkit-scrollbar-thumb {
    background: #f0f0f0;
    border: 3px solid #fff;
    border-radius: 9px;
  }
}

.alert {
  box-shadow: 0 0 0;
  padding: 0.65rem 1.25rem;
  border: 2px solid transparent !important;
  .close {
    color: $primary;
    opacity: .7;
  }
}

.alert-block {
  ul {
    margin-bottom: 0;
    margin-top: 1.25em;
  }
}

.alert-danger {
  color: $red;
  background: lighten($red, 43%);
}

.alert-warning {
  color: darken($yellow, 10%);
  background: lighten($yellow, 45%);
}

.alert-success {
  color: $green;
  background: lighten($green, 65%);
}

.alert-info {
  color: $blue;
  background: lighten($blue, 45%);
}

.excaptcha-image {
  width: 120px;
}

/* Bootstrap Theme */

body {
  background: #e5e5e5;
  color: $primary;
  font-family: Helvetica, Arial, "PingFang SC", "Noto Sans", Roboto, "Microsoft Yahei", sans-serif;
  font-size: 14px;
  letter-spacing: .03em;
}

#main {
  margin-top: 20px;
}

.sub-navbar {
  background: #f9f9f9;
  box-shadow: 0 1px 0px rgba(0, 0, 0, 0.02);
  a {
    color: $blue;
  }
}

.btn-default {
  border-color: $input-border-color;
  background: #fcfcfc;
  color: $primary !important;
  &:hover {
    border-color: #ccc;
  }
  &:active,
  &.active {
    color: #aaa;
    border-color: $default;
    background: #f0f0f0;
  }
}

.page-topics {
  .navbar-fixed-active {
    .navbar-topic-title {
      display: block;
    }
    .navbar-topic-title:after {
      display: block;
    }
  }
}

.list-group-item {
  a {
    color: $primary;
  }
}

.sidebar.col-md-3 {
  padding-left: 0;
}

.nav-tabs {
  li:first-child {
    margin-left: 15px;
  }
  .nav-item {
    .nav-link {
      &:hover {
        background: #f2f2f2;
        border-color: #f2f2f2;
      }
    }
  }
}

.close {
  font-size: 1.2rem;
  font-weight: normal;
}

.pagination {
  margin: 0;
  &>li:first-child>a,
  &>li:first-child>span {
    border-top-left-radius: 3px;
    border-bottom-left-radius: 3px;
  }
  &>li:last-child>a,
  &>li:last-child>span {
    border-top-right-radius: 3px;
    border-bottom-right-radius: 3px;
  }
  li>a {
    color: $primary;
  }
  li>a,
  .disabled>a,
  li>span {
    border-color: #E0E0E0 !important;
  }
  li>a:hover {
    color: #555;
    background: $gray;
  }
  li.active>a,
  li.active>a:hover {
    background-color: $blue !important;
    border-color: $blue !important;
    color: #FFF !important;
  }
}

.pager {
  margin: 0px;
  .info {
    line-height: 32px;
    color: #ccc;
    samp {
      color: #999;
    }
  }
  li>a,
  li>span {
    color: #666;
    border-radius: 3px;
    border: 0px;
    background: transparent;
    &:hover {
      background: #fff;
    }
  }
  li.disabled>a,
  li.disabled>span {
    color: #ddd;
    background: transparent;
    &:hover {
      color: #ddd;
      background: transparent;
    }
  }
}

abbr {
  text-decoration: none;
  border-bottom: 0px;
  cursor: pointer;
}

abbr[title] {
  border: 0 !important;
  text-decoration: none;
  cursor: text;
}

a abbr[title] {
  cursor: pointer;
}

kbd {
  background-color: #f5f5f5;
  color: #999;
  border-radius: 2px;
  border-color: #fafafa;
  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.05);
}

.table>tbody>tr>td,
.table>tbody>tr>th,
.table>tfoot>tr>td,
.table>tfoot>tr>th,
.table>thead>tr>td,
.table>thead>tr>th {
  padding: 4px 5px;
}

.card {
  margin-bottom: 15px;
  border-top: 0;
  box-shadow: 0 0 0;
  border-color: #e5e6e9 #dfe0e4 #d0d1d5;
  .card-header,
  .card-footer {
    padding: 6px 15px;
  }
  .card-body,
  .list-group-item {
    padding: 10px 15px;
  }
  .card-header {
    background: #fafafa;
    border-bottom-color: #eee;
    color: #777;
  }
  .card-footer {
    border-top-color: #eee;
    background: #fafafa;
  }
}

.nav-link {
  color: $primary;
  &:hover {
    color: $primary;
  }
}

.nav-stacked.nav-pills>li>a {
  border-radius: 0px;
  i.fa {
    width: 20px;
  }
}

/* Modal */

.modal {
  top: 50px;
  .modal-content {
    border: 0px;
    box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.15), 0px 0px 1px 1px rgba(0, 0, 0, 0.05);
    p {
      margin-bottom: 5px;
    }
  }
  .modal-header {
    padding: 8px 15px;
    border-bottom: 0px;
    .modal-title {
      font-size: 16px;
      font-weight: bold;
    }
    .close {
      font-size: 1.2rem;
      font-weight: normal;
    }
  }
  .modal-footer {
    border-color: #e5e5e5;
  }
  @media (min-width: 768px) {
    .modal-dialog {
      width: 768px;
    }
  }
}

.modal-backdrop.in {
  opacity: 1;
  background-color: rgba(0, 0, 0, 0.1);
}

.list-group {
  .list-group-item {
    border-color: #eee;
  }
}

.fa-spin {
  -webkit-animation: fa-spin 0.8s infinite linear;
  animation: fa-spin 0.8s infinite linear;
}

/* App Style */

.opts {
  color: #666;
  a {
    display: inline-block;
    border-radius: 3px;
  }
  a:link,
  a:visited {
    color: $primary;
    line-height: 100%;
    padding: 4px;
    text-decoration: none;
  }
  a:hover {
    background: #eee;
    text-decoration: none;
  }
}

.turbolinks-progress-bar {
  background-color: $blue !important;
  height: 2px !important;
}

.pull-right.opts {
  a {
    margin-left: 5px;
    margin-right: 0px;
  }
}

.avatar {
  .uface,
  .media-object {
    border-radius: 120px;
  }
}

.avatar-16 {
  width: 16px;
  height: 16px;
  border-radius: 120px;
}

.avatar-32 {
  width: 28px;
  height: 28px;
  border-radius: 120px;
}

.avatar-48 {
  width: 40px;
  height: 40px;
  border-radius: 120px;
}

.avatar-96 {
  width: 96px;
  height: 96px;
  border-radius: 120px;
}

@media (max-width: 480px) {
  .avatar-48 {
    width: 28px;
    height: 28px;
  }
}

.uname {
  color: #666;
}

img.emoji {
  width: 20px;
  height: 20px;
}

.node-name {
  background: #f0f0f0;
  padding: 1px 3px;
  color: #777;
  margin-right: 5px;
  &:hover {
    color: #555;
    text-decoration: none;
    background: #e0e0e0;
  }
}

.fa.awesome {
  font-size: 13px;
  color: $red;
}

.notification-count {
  .count {
    margin-left: 4px;
    display: none;
    font-size: 12px;
  }
  .new {
    color: $red !important;
    .count {
      display: inline;
      line-height: 100%;
    }
  }
}

.deleted {
  text-decoration: line-through;
  color: #e0e0e0;
}

.no-result {
  color: #aaa;
  padding-bottom: 20px;
  text-align: center;
}

.opts a.active {
  .fa {
    @extend .animated;
    @extend .bounceIn;
    color: $red;
  }
}

.setting-menu {
  padding-right: 0;
}

.editor-toolbar {
  margin-bottom: 5px;
  .opts {
    a.nav-link {
      border: 1px solid $input-border-color;
      @extend .btn-default;
      font-size: 14px;
      line-height: 14px;
      display: inline-block;
      padding: 8px;
      margin: 0;
    }
  }
  .reply-to {
    margin-top: 10px;
    font-size: 14px;
    line-height: 14px;
    .fa {
      margin-right: 8px;
    }
    .close {
      font-size: 14px;
      margin-left: 5px;
    }
  }
}

@media (max-width: 480px) {
  body {
    font-size: 14px;
    line-height: 100%;
  }
  .container {
    padding: 0;
  }
  .card {
    border-radius: 0;
    border-left: 0px;
    border-right: 0px;
  }
  #main {
    margin-top: 1px;
  }
  .opts {
    a {
      padding: 5px !important;
      margin: 0 !important;
    }
  }
  .row {
    margin: 0;
    .col-md-9,
    .col-md-8,
    .col-md-6,
    .col-md-5,
    .col-md-4,
    .col-md-3 {
      padding: 0;
    }
  }
  .sidebar {
    display: none;
  }
  .hidden-mobile {
    display: none !important;
  }
  .topics {
    .topic {
      padding: 8px !important;
      margin: 0 -8px !important;
      .avatar {
        margin-top: 8px;
      }
      .count {
        font-size: 12px;
        width: 38px !important;
      }
    }
  }
  .pagination {
    display: block;
    li {
      display: none;
    }
    li.prev,
    li.next {
      float: left;
      display: block;
      a {
        border-radius: 20px !important;
      }
    }
    li.next {
      float: right;
    }
  }
}

.node-list {
  .node {
    margin-top: 0px;
    &:last-child {
      margin-bottom: 0px;
    }
    .media-left {
      min-width: 120px;
    }
    label {
      font-weight: normal;
      color: #aaa;
      text-align: right;
    }
    .name {
      margin-bottom: 10px;
      width: 100px;
      display: block;
      float: left;
      text-align: left;
      a:link,
      a:visited {
        color: #333;
      }
    }
  }
}

.navbar {
  &.fixed-title {
    .navbar-topic-title {
      display: none;
    }
  }
  .navbar-topic-title {
    display: none;
  }
}

@media (min-width: 767px) {
  .navbar {
    &.fixed-title {
      .navbar-topic-title {
        display: block;
      }
      #main-nav-menu {
        display: none;
      }
      .nav-search {
        display: none;
      }
    }
    .navbar-topic-title {
      display: none;
      height: 50px;
      text-align: left;
      overflow: hidden;
      top: 0;
      position: absolute;
      background: #FFF;
      display: none;
      min-width: 400px;
      &:after {
        clear: all;
        display: block;
      }
      a.topic-title {
        display: inline;
        text-decoration: none;
        overflow: hidden;
        line-height: 0;
        max-width: 0px;
        color: #000;
        &:hover,
        &:active,
        &:visited {
          color: #000;
        }
        i.fa {
          color: #999;
          margin-left: 3px;
        }
        i.fa-diamond {
          color: $red;
        }
        i.fa-check {
          color: $green;
        }
      }
      .node {
        line-height: 50px;
        margin-left: 16px;
        color: #777;
        margin-right: 3px;
      }
      h1 {
        margin: 0;
        padding: 0;
        font-size: 16px;
        line-height: 50px;
      }
    }
  }
}

@media (min-width: 992px) {
  .navbar {
    .navbar-topic-title {
      a.topic-title {
        max-width: 450px;
      }
    }
  }
}

@media (min-width: 1200px) {
  .navbar {
    .navbar-topic-title {
      a.topic-title {
        max-width: 640px;
      }
    }
  }
}

.move-page-buttons {
  position: fixed;
  bottom: 10px;
  right: 10px;
  width: 45px;
  .btn {
    background: #fff;
  }
}

.node-header {
  .container {
    padding: 5px 30px;
  }
  @media (max-width: 480px) {
    .container {
      padding: 5px 8px;
    }
    .filter {
      .all-nodes {
        display: none;
      }
    }
  }
  .title {
    font-size: 24px;
    color: #333;
    margin-bottom: 8px;
    .total {
      color: #999;
      font-size: 14px;
      margin-left: 10px;
    }
  }
  .summary {
    p:last-child {
      margin-bottom: 0;
    }
  }
  .filter {
    &>li {
      margin-right: 0px;
      &.active {
        a:link,
        a:visited,
        a:hover {
          color: #000;
          text-decoration: none;
          border-bottom: 2px dotted #666666;
        }
      }
      &>a {
        background: transparent !important;
        border-radius: 0px;
        line-height: 100%;
        padding: 8px 8px;
        margin-right: 5px;
        font-size: 14px;
        border-bottom: 2px dotted transparent;
        text-decoration: none;
        display: inline-block;
        color: #606060;
        &.active {
          color: #000;
          text-decoration: none;
          border-bottom: 2px dotted #666666;
        }
        &:hover {
          border-color: #eee;
          background: transparent;
        }
        &.all-nodes {
          border-radius: 3px;
          outline: 0 !important;
          margin-right: 1.25rem;
          background: #f0f0f0;
          border: 0;
          .caret-right {
            display: inline-block;
            width: 0;
            height: 0;
            margin-left: 10px;
            vertical-align: middle;
            border-left: 4px solid;
            border-top: 4px solid transparent;
            border-bottom: 4px solid transparent;
          }
          &:hover {
            border: 0;
            color: #222;
          }
        }
      }
    }
  }
  .prefix-filter {
    @extend .filter;
    &>li {
      margin-right: 40px;
      &>a {
        padding: 0px 0px;
      }
    }
  }
}

.topics-node {
  .node {
    display: none;
  }
}

.topics {
  .no-result {
    padding-bottom: 0;
    margin-bottom: 0;
    padding: 100px 0;
  }
  .topics-group:first-child {
    @media (max-width: 991px) {
      .topic:last-child {
        border-bottom: 1px solid #F0F0F0;
      }
    }
  }
  .topic {
    min-height: 68px;
    border-bottom: 1px solid #F0F0F0;
    padding: 10px 15px;
    margin: 0 -15px;
    vertical-align: top;
    &:first-child {
      padding-top: 0;
    }
    &:last-child {
      border-bottom: 0px;
    }
    .avatar {
      padding-top: 6px;
      margin-right: 10px;
    }
    .title {
      font-size: 15px;
      margin-bottom: 0;
      a:link,
      a:visited {
        color: #222;
        font-weight: 400;
        line-height: 30px;
        word-break: break-all;
        .node {
          color: #777;
          margin-right: 3px;
        }
      }
      a:active,
      a:hover {
        color: #555;
        .node {
          color: #555;
        }
        text-decoration: none;
      }
      i.fa {
        color: #999;
        margin-left: 3px;
      }
      i.fa-diamond {
        color: $red;
      }
      i.fa-check {
        color: $green;
      }
    }
    .info {
      color: #adaaa8;
      font-size: 13px;
      margin-top: 0;
      a {
        color: #797776;
        text-decoration: underline;
      }
    }
    .count {
      width: 100px;
      text-align: right;
      padding-top: 1.25rem;
      a:link,
      a:hover,
      a:visited {
        line-height: 11px;
        color: #fff;
        font-size: 13px;
        min-width: 32px;
        text-align: center;
        border-radius: 80px;
        padding: 3px 8px 3px 8px;
        display: inline-block;
        text-decoration: none;
      }
      a:link {
        background: rgba(79, 147, 248, 0.24);
      }
      a:hover {
        background: #f0f0f0;
      }
      a.state-true,
      a:visited {
        background: #f0f0f0;
      }
    }
  }
}

.topic-detail {
  margin-bottom: 15px;
  .card-body {
    padding-top: 20px;
    padding-bottom: 20px;
  }
  .card-header {
    padding: 15px;
    transition: all .3s;
    h1 {
      margin-top: 0;
      font-size: 20px;
      color: #333;
      text-align: left;
      line-height: 150%;
      margin-bottom: 8px;
      .node {
        color: #777;
        margin-right: 3px;
      }
      i.fa-check {
        color: $green;
        font-size: 16px;
      }
    }
    .avatar {
      text-align: right;
    }
  }
  .label-awesome {
    font-size: 16px;
    background: lighten($red, 45%);
    border-bottom: 1px solid lighten($red, 42%);
    padding: 10px 15px;
    color: $red;
    .fa {
      font-size: 16px;
    }
    a {
      color: #aAa5a4;
    }
  }
  .info {
    color: #adaaa8;
    font-size: 13px;
    a {
      color: $primary;
    }
    .node {
      color: $primary;
      font-weight: bold;
    }
    .user-name {
      color: $primary;
      font-size: 14px;
    }
    .team-name {
      font-size: 14px;
      color: $green;
    }
    em {
      font-style: normal;
    }
    .opts {
      a {
        margin-left: 5px;
        color: $primary;
      }
    }
  }
}

#topic-sidebar {
  position: fixed;
  display: none;
  width: 260px;
  @media (min-width: 960px) {
    display: block;
    width: 242px;
  }
  @media (min-width: 1200px) {
    display: block;
    width: 292px;
  }
  .group {
    text-align: center;
    margin-bottom: 20px;
  }
  .buttons {
    margin-top: 20px;
    .likes {
      a {
        display: block;
        width: 90px;
        margin: 0 auto;
        border-radius: 5px;
        padding: 10px 0;
      }
      a:link,
      a:hover,
      a:visited {
        text-decoration: none;
        color: $primary;
      }
      a:hover {
        background: rgba(0, 0, 0, 0.03);
      }
      i.fa {
        display: block;
        font-size: 40px;
        color: $primary;
      }
      a.active {
        i.fa {
          color: $red;
        }
      }
      span {
        display: block;
        color: #666;
      }
    }
  }
  .reply-buttons {
    text-align: center;
    .total {
      margin-bottom: 10px;
    }
  }
  a.btn-move-page {
    color: $primary;
  }
}

#replies {
  margin-bottom: 15px;
  .card-body {
    padding-top: 0px;
    padding-bottom: 0px;
  }
  .info {
    .uname {
      color: $primary;
    }
    .opts {
      a {
        font-size: 14px;
        margin-left: 5px;
        color: $primary;
      }
      a.edit {
        display: none;
      }
    }
  }
  .reply {
    margin: 0 -15px;
    padding: 15px;
    position: relative;
    border-bottom: 1px solid #eee;
    padding-left: 65px;
    &.reply-system,
    &.reply-deleted {
      padding: 10px 15px;
      font-size: 15px;
      color: #666;
      border-bottom: 1px solid #F0F0F0;
      img.media-object {
        border-radius: 180px;
        display: inline-block;
        margin-right: 3px;
        vertical-align: text-bottom;
      }
      .time {
        margin-left: 4px;
        color: #aaa;
      }
      .ban-reason {
        color: #444;
        border-bottom: 1px dashed #eee;
      }
    }
    .infos {
      min-height: 48px;
    }
    .avatar {
      position: absolute;
      top: 17px;
      left: 15px;
    }
    &:last-child {
      border-bottom: 0px;
    }
    &.none {
      text-align: center;
      color: #999;
      min-height: 32px;
    }
    &.light {
      background: #F7F2FC;
    }
    &.popular {
      background: #fffce9;
    }
    .info {
      .name {
        font-size: 14px;
        a {
          color: $primary;
        }
      }
      color: #999;
      margin-bottom: 10px;
      font-size: 13px;
      .floor {
        color: #7AA87A;
      }
      a.time {
        color: #999;
        text-decoration: none !important;
        cursor: pointer;
        &:hover {
          border-bottom: 1px dashed #ccc;
        }
      }
    }
    .opts {
      a {
        display: inline-block;
        vertical-align: baseline;
        line-height: 22px;
        padding: 2px 5px;
        height: 22px;
        min-width: 22px;
        text-align: center;
      }
    }
    .reply-to-block {
      padding: 10px 1.25rem;
      background: #f7f7f7;
      border-radius: 3px;
      margin-bottom: 1.25rem;
      .info {
        margin: 0;
        a {
          color: $primary;
        }
        .media-object {
          display: inline-block;
          margin-right: 5px;
          vertical-align: middle;
        }
      }
      .markdown {
        margin-top: 10px;
        font-size: 15px;
        p {
          font-size: 15px;
        }
      }
    }
    .markdown {
      pre {
        margin-right: 0px;
        margin-left: 0px;
      }
    }
    @media (min-width: 1026px) {
      .hideable {
        display: none;
      }
    }
    &:hover {
      .hideable {
        display: inline-block;
      }
    }
  }
  @media (max-width: 480px) {
    .reply {
      padding: 10px;
      margin: 0 -10px;
      padding-left: 65px;
    }
  }
}

.edit-reply {
  form {
    .btn-primary {
      margin-right: 10px;
    }
  }
}

#node-selector {
  .card {
    border: 0;
    box-shadow: 0 0 0;
    padding: 0;
    margin: 0;
  }
  .card-header {
    display: none;
  }
  .card-body {
    padding: 0 20px;
    margin: 0;
  }
}

#node-selector-button {
  border-color: #ced4da;
}

#notifications {
  .card-header {
    .clean-button {
      margin-left: 10px;
    }
  }
  .notification {
    position: relative;
    margin-bottom: 1.25rem;
    padding-bottom: 1.25rem;
    border-bottom: 1px solid $gray;
    &:last-child {
      margin-bottom: 0px;
      border-bottom: 0px;
      padding-bottom: 0px;
    }
    .unread {
      color: $blue;
      font-size: 10px;
      position: absolute;
      right: 5px;
      top: 20px;
    }
    .avatar {
      text-align: center;
    }
    .info {
      color: #999;
      margin-bottom: 8px;
      font-size: 14px;
    }
    .date {
      font-size: 14px;
      color: #aaa;
    }
  }
}

.sidebar {
  .card {
    margin-bottom: 1.25rem;
  }
  .card-body {
    word-break: break-all;
  }
}

.api-doc {
  .route-list {
    padding: 20px 0;
    border-right: 2px dashed #ddd;
    li {
      line-height: 200%;
      color: #999;
      a:link,
      a:visited {
        color: #404040 !important;
        text-decoration: underline !important;
      }
    }
  }
  .route {
    margin-top: 1.25rem;
    h5 {
      color: #333;
      border-bottom: 1px solid $gray;
      margin: 0;
      margin-bottom: 10px;
      padding: 5px 0 0 0;
      label {
        font-size: 12px;
        font-weight: normal;
        display: inline-block;
        width: 50px;
        color: $gray;
      }
    }
    .content {
      margin: 0 1.25rem;
    }
    .desc {
      h4 {
        border: 0px;
        font-size: 13px !important;
        margin: 0;
        color: #999;
      }
    }
    h6 {
      color: #999;
    }
    table.params {
      td.field {
        width: 80px;
      }
      td.type {
        width: 70px;
      }
      td.required {
        width: 50px;
      }
      td.values {
        width: 180px;
      }
      td.default {
        width: 100px;
      }
    }
  }
}

// Fix searchbox style
.bs-searchbox .form-control {
  float: none;
}

@media (min-width: 744px) and (max-width: 1200px) {
  .sidebar .card .card-body .feed-button {
    float: none !important;
    margin-top: 1.25rem;
  }
}

/* Social Share Button */

.social-share-button {
  height: 16px;
  a {
    i.fa {
      font-size: 24px;
      margin: 0 4px;
    }
    &:link,
    &:visited {
      color: #777;
    }
    &:hover {
      color: $blue;
    }
  }
}

.popover-content {
  .social-share-button {
    display: block;
  }
}

/* Markdown Styles */

.markdown {
  position: relative;
  letter-spacing: .03em;
  font-size: 15px;
  text-overflow: ellipsis;
  word-wrap: break-word;
  a {
    color: $blue;
  }
  img,
  iframe {
    max-width: 100%;
    border: 0;
  }
  p,
  pre,
  ul,
  ol,
  hr,
  blockquote {
    margin-bottom: 20px;
  }
  p:last-child,
  blockquote:last-child,
  pre:last-child,
  ul:last-child,
  ol:last-child,
  hr:last-child {
    margin-bottom: 0;
  }
  p {
    font-size: 15px;
    line-height: 26px;
  }
  hr {
    border: 2px dashed $gray;
    border-bottom: 0px;
    margin-left: auto;
    margin-right: auto;
    width: 50%;
  }
  blockquote {
    margin-left: 0 15px 15px 15px;
    padding: 0;
    padding-left: 32px;
    border: 0px;
    quotes: "\201C""\201D""\2018""\2019";
    position: relative;
    font-size: 15px;
    line-height: 1.45;
    p {
      display: inline;
      color: #999;
    }
    &:before,
    &:after {
      display: block;
      content: "\201C";
      font-size: 35px;
      position: absolute;
      font-family: serif;
      left: 0px;
      top: 0px;
      color: #aaa;
    }
  }
  pre {
    font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
    font-size: 12px;
    background-color: #f9f9f9;
    border: 0px;
    border-top: 1px solid #f0f0f0;
    border-bottom: 1px solid #f0f0f0;
    margin: 0 -15px 15px -15px;
    padding: 10px 15px;
    color: #444;
    overflow: auto;
    border-radius: 0px;
    code {
      display: block;
      line-height: 150%;
      padding: 0 !important;
      font-size: 12px !important;
      background-color: #f9f9f9 !important;
      border: none !important;
    }
  }
  code {
    display: inline-block;
    font-size: 12px !important;
    background-color: #f5f5f5 !important;
    border: 0px;
    color: #444 !important;
    padding: 1px 4px !important;
    margin: 2px;
    border-radius: 3px;
    word-break: break-all;
    line-height: 20px;
  }
  a:link,
  a:visited {
    color: $blue !important;
    text-decoration: none !important;
  }
  a:hover {
    text-decoration: underline !important;
    color: $primary !important;
  }
  a.mention-floor {
    color: #60b566 !important;
    margin-right: 3px;
  }
  a.mention {
    color: #777 !important;
    font-weight: bold;
    margin-right: 2px;
    b {
      color: #777 !important;
      font-weight: normal;
    }
  }
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    font-weight: bold;
    text-align: left;
    margin-top: 0px;
    margin-bottom: 20px;
  }
  h1 {
    font-size: 26px !important;
    text-align: center;
    margin-bottom: 30px !important;
  }
  h2,
  h3,
  h4 {
    text-align: left;
    font-weight: bold;
    font-size: 16px !important;
    line-height: 100%;
    margin: 0;
    color: #555;
    margin-bottom: 20px;
    border-bottom: 1px solid #eee;
    padding-bottom: 10px;
  }
  h2 {
    font-size: 20px !important;
    color: #111;
  }
  h3 {
    font-size: 18px !important;
    color: #333;
  }
  h5,
  h6 {
    font-size: 15px;
    line-height: 100%;
    color: #777;
  }
  h6 {
    font-size: 14px;
    color: #999;
  }
  strong {
    color: #000;
  }
  ul,
  ol {
    list-style-type: square;
    margin: 0;
    margin-bottom: 20px;
    padding: 0px 20px;
    p,
    blockquote,
    pre {
      margin-bottom: 8px;
    }
    li {
      line-height: 1.6em;
      padding: 2px 0;
      color: #333;
    }
    ul {
      list-style-type: circle;
      margin-bottom: 0px;
    }
  }
  ol {
    list-style-type: decimal;
    ol {
      list-style-type: lower-alpha;
      margin-bottom: 0px;
    }
  }
  img {
    vertical-align: top;
    max-width: 100%;
  }
  a.zoom-image {
    cursor: zoom-in;
  }
  a.at_floor {
    color: #60B566 !important;
  }
  a.at_user,
  a.user-mention {
    color: #0069D6 !important;
  }
  img.twemoji {
    width: 20px;
  }
}

.notify-updated {
  display: none;
  padding: 4px 15px;
  margin-bottom: 20px;
  text-align: left;
  background: #FDF8A6;
  border: 1px solid #F5E3A4;
  color: $red;
  a:link,
  a:visited {
    color: $yellow;
  }
}

.dz-preview {
  display: none;
}

textarea.div-dropzone-focus {
  border-color: #BBE1C9;
  background: #fafafa;
}

#dropdown-insert-codes {
  .dropdown-toggle::after {
    display: none;
  }
}

.emoji-modal {
  .modal-dialog {
    max-width: 496px;
  }
  .modal-header {
    border: 0px;
    padding: 8px;
  }
  .modal-body {
    padding: 0 8px 8px 8px;
  }
  .twemoji {
    width: 20px;
    height: 20px;
  }
  .nav>li>a {
    padding: 5px 8px;
  }
  .nav-tabs li:first-child {
    margin-left: 8px;
  }
  .tab-pane {
    padding: 0px;
    height: 180px;
    overflow: scroll;
    &::-webkit-scrollbar {
      width: 4px;
      border-radius: 3px;
    }
    &::-webkit-scrollbar-thumb {
      background: #e0e0e0;
    }
    a {
      padding: 5px;
      display: inline-block;
      width: 30px;
      height: 30px;
      &:hover {
        background: #f0f0f0;
      }
    }
  }
  .modal-footer {
    padding: 8px;
    text-align: left;
    font-size: 16px;
    .emoji {
      width: 48px;
      height: 48px;
      margin-right: 10px;
    }
  }
}

.popover-liked-users {
  .avatar-16 {
    display: inline-block;
    margin: 5px 0;
  }
}

.comments {
  .comment {
    padding: 15px;
    margin: 0 -15px;
    border-bottom: 1px solid #f0f0f0;
    &:first-child {
      padding-top: 0;
    }
    &:last-child {
      border: 0;
      padding-bottom: 0;
    }
    .info {
      font-size: 13px;
      color: #999;
    }
  }
}

.card-body {
  .heading {
    font-size: 16px;
    color: #777;
    font-weight: bold;
    padding-bottom: 10px;
    border-bottom: 1px solid #eee;
    margin-bottom: 15px;
  }
  form {
    margin-bottom: 25px;
    &:last-child {
      margin-bottom: 0;
    }
  }
}

.reward-image {
  border-radius: 3px;
  padding: 20px;
  background: #FFF;
  text-align: center;
  img {
    max-width: 240px;
  }
}

#reward-modal {
  padding-top: 50px;
  text-align: center;
  .modal-dialog {
    max-width: 750px;
    min-height: 300px;
  }
  .reward-image {
    display: inline-block;
    padding: 0px 20px 10px 20px;
  }
  .message {
    margin: 10px auto 0 auto;
    max-width: 580px;
    font-size: 16px;
    text-align: center;
    .user-info {
      margin-bottom: 15px;
    }
    .media-object {
      display: inline-block;
    }
    i.fa {
      color: #aaa;
    }
  }
}

.alert {
  box-shadow: 0 0 0;
  padding: 0.65rem 1.25rem;
  border: 2px solid transparent !important;
  .close {
    color: $primary;
    opacity: .7;
  }
}

.alert-block {
  ul {
    margin-bottom: 0;
    margin-top: 1.25em;
  }
}

.alert-danger {
  color: $red;
  background: lighten($red, 43%);
}

.alert-warning {
  color: darken($yellow, 10%);
  background: lighten($yellow, 45%);
}

.alert-success {
  color: $green;
  background: lighten($green, 65%);
}

.alert-info {
  color: $blue;
  background: lighten($blue, 45%);
}

================================================
FILE: assets/css/app/components/page.scss
================================================
#home_index {
  line-height: 160%;
}

.home-icons {
  .item {
    text-align: center;
    margin-bottom: 15px;
    border-radius: 3px;
    background: #FFF;
    border: 1px;
    border-color: #e5e6e9 #dfe0e4 #d0d1d5 #dfe0e4;
    .icon {
      display: block;
      a {
        display: block;
        padding: 20px 15px;
      }
      .fa {
        font-size: 60px;
      }
    }
    .text {
      display: block;
      text-align: left;
      background: #F5F5F5;
      border-top: 1px solid #E9E9E9;
      font-size: 14px;
      font-weight: bold;
      a {
        display: block;
        color: #666;
        padding: 6px 15px;
      }
      a:hover {
        text-decoration: none;
      }
      .fa {
        margin-top: 3px;
      }
      border-radius: 0 0 3px 3px;
    }
    &:hover {
      opacity: 0.75;
    }
  }
  .item1 {
    .icon {
      .fa {
        color: lighten($grape, 10%);
      }
    }
  }
  .item3 {
    .icon {
      .fa {
        color: lighten($grape, 10%);
      }
    }
  }
  .item2 {
    .icon {
      .fa {
        color: lighten($yellow, 10%);
      }
    }
  }
  .item4 {
    .icon {
      .fa {
        color: lighten($green, 10%);
      }
    }
  }
}

h2 {
  font-size: 12px;
  color: #999;
  line-height: 100%;
  margin-bottom: 10px;
  text-align: center;
}

#last_topics {
  float: left;
  width: 450px;
}

#hot_topics {
  float: right;
  width: 450px;
}

.node-topics {
  margin-bottom: 0px;
  .head {
    display: none;
  }
}

.location-list {
  .name {
    a {
      color: #666;
      margin: 6px;
      display: inline-block;
    }
  }
}

.home_suggest_topics {
  .topics {
    .topic {
      .title {
        height: 30px;
        overflow: hidden;
      }
    }
  }
}

================================================
FILE: assets/css/app/components/user.scss
================================================
.subnav {
  margin-bottom: -18px;
  .nav-tabs {
    border-bottom: 0px;
    padding-left: 20px;
  }
  .nav-tabs>li>a:hover {
    border-color: transparent;
    background: none;
    text-decoration: underline;
  }
  .nav-tabs>.active>a,
  .nav-tabs>.active>a:hover {
    color: #555555;
    background-color: #ffffff;
    border: 1px solid #ddd;
    border-bottom-color: transparent;
    cursor: default;
  }
}

.page-users {
  .nav-tabs {
    border-bottom: 0px;
  }
  @media (max-width: 480px) {
    .sidebar {
      display: block;
    }
  }
}

.node-topics {
  border-bottom: 1px solid #ddd;
  tbody>tr>td {
    padding: 8px;
    color: #666;
  }
  td.title {
    a:link,
    a:visited {
      font-size: 15px;
      text-decoration: none;
    }
    a:hover {
      text-decoration: underline;
    }
    em {
      font-style: normal;
      font-size: 12px;
      color: #bbb;
    }
    i.icon {
      margin-bottom: -1px;
    }
  }
  td.node {
    a {
      color: #666;
    }
  }
  tr.head {
    td {
      border-top: none;
      padding-top: 14px;
      color: #CCC;
      font-weight: bold;
      font-size: 12px;
    }
  }
  tr.odd {
    td {
      background: #fafafa;
    }
  }
  tr.topic {
    td.author {
      width: 80px;
      a {
        color: #666;
        font-weight: bold;
      }
    }
  }
}

.recent-topics {
  ul {
    li {
      .title {
        font-size: 14px;
      }
      i.icon {
        margin-bottom: -1px;
      }
      i.fa-diamond {
        color: $red;
      }
      .info {
        margin-top: 3px;
        font-size: 12px;
        color: #bbb;
      }
      .node {
        margin-right: 5px;
        color: #777;
        margin-right: 3px;
      }
    }
  }
}

.recent-replies {
  margin-bottom: 0;
  li {
    .title {
      font-size: 15px;
      .info {
        font-size: 14px;
        color: lighten($secondary, 20%);
      }
    }
    .body {
      a {
        color: $primary;
      }
      margin-top: 6px;
      color: $secondary;
      p {
        font-size: 14px;
      }
      img {
        max-width: 680px;
      }
    }
  }
}

.row>.span13 {
  margin-left: 0;
}

#main .userinfo h1 {
  text-align: left;
  display: inline;
}

.userinfo {
  .tagline {
    text-align: left;
    margin-top: -8px;
    margin-bottom: 20px;
  }
  .media-right {
    padding-left: 15px;
    text-align: center;
    .avatar {
      margin-bottom: 10px;
    }
  }
  .list-group {
    margin-bottom: 0px;
  }
  li {
    border-color: #f0f0f0;
    font-size: 13px;
    label {
      color: #999;
      margin-right: 8px;
      display: inline-block;
      width: 80px;
      text-align: right;
    }
  }
}

.bio {
  font-size: 12px;
  line-height: 180%;
  p:last-child {
    margin-bottom: 0;
  }
}

.replies ul {
  margin: 0;
  h6 {
    color: #999;
    font-weight: normal;
  }
  li {
    line-height: 180%;
    border-bottom: 1px solid #ddd;
    list-style: none;
  }
  blockquote {
    line-height: 160%;
  }
}

.content>.tabs {
  border-bottom: 2px solid #ccc;
  .active {
    margin-bottom: 0;
  }
}

table.node-topics {
  td {
    a {
      color: #333;
      font-weight: normal;
    }
    i.fa-diamond {
      color: $grape;
    }
  }
  td.replied-at {
    width: 80px;
  }
}

.user-list {
  h2 {
    font-size: 14px;
    margin: 0;
  }
  .user {
    text-align: center;
    margin-bottom: 20px;
    overflow: hidden;
    ;
    .avatar {
      img {
        width: 48px;
        height: 48px;
        margin: 0 auto;
      }
      margin-bottom: 5px;
    }
    .name {
      a {
        color: #333;
      }
    }
  }
}

.bloced-users {
  .item {
    text-align: left;
    margin-bottom: 10px;
    .media-object {
      display: inline;
    }
  }
}

.sidebar {
  padding-right: 0;
  .profile {
    .avatar {
      .level {
        margin-top: 6px;
        text-align: center;
      }
    }
    .item {
      margin-bottom: 5px;
    }
    .item a {
      color: #666;
    }
    .number {
      color: #999;
    }
    .counts {
      color: #999;
      span {
        color: #666;
      }
    }
    .follow-info {
      border-top: 1px solid #f0f0f0;
      text-align: center;
      margin-top: 15px;
      padding-top: 15px;
      a {
        display: block;
        text-decoration: none;
      }
      a.counter {
        font-size: 32px;
        color: $primary;
        &:hover {
          color: $primary;
        }
      }
      a.text {
        color: #999;
      }
    }
    .buttons {
      border-top: 1px solid #f0f0f0;
      margin-top: 15px;
      padding-top: 15px;
    }
    .social {
      font-size: 18px;
      a {
        color: #999;
        margin-right: 8px;
      }
      a:hover {
        color: #666;
      }
    }
    .tagline {
      border-top: 1px solid #f0f0f0;
      margin-top: 10px;
      color: #999;
      line-height: 100%;
      padding: 10px;
      padding-bottom: 0;
    }
  }
  .user-teams {
    .media-object {
      display: inline-block;
      margin: 4px 2px;
    }
  }
}

.user-card {
  margin-bottom: 15px;
  padding-left: 15px;
  .media-heading {
    font-weight: bold;
    a {
      color: #333;
    }
  }
  .infos {
    color: #999;
    font-size: 12px;
    .item {
      margin-top: 5px;
    }
  }
}

.user-profile-fields {
  margin-top: 20px;
  border-top: 1px solid #eee;
  padding-top: 20px;
  .field {
    padding: 2px 0;
    label {
      color: #666;
      display: inline-block;
      width: 100px;
      margin-right: 10px;
    }
    .value {
      a {
        text-decoration: underline;
      }
    }
  }
}

#user_github_repos {
  .more {
    text-align: right;
  }
  ul {
    margin: 0;
  }
  li {
    .title {
      position: relative;
      margin-bottom: 5px;
      a {
        color: #333;
        font-weight: bold;
      }
      .watchers {
        position: absolute;
        top: 2px;
        right: 0;
        color: #999;
      }
    }
    .desc {
      font-size: 12px;
      color: #888;
      padding: 0;
      margin: 0;
    }
  }
}

.user-activity-graph {
  overflow-x: scroll;
  text-align: center;
  svg {
    margin: 0 auto;
  }
}

.avatar-preview {
  .media-object {
    display: inline-block;
  }
}

================================================
FILE: assets/css/app.scss
================================================
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome.scss";
@import "~bootstrap/scss/bootstrap";
@import "./common/variables/mipha";
@import "./common/components/header";
@import "./app/components/footer";
@import "./app/components/base";
@import "./app/components/page";
@import "./app/components/user";
@import "./app/components/notification";
// import jquery.atwho
@import "../static/atwho/jquery.atwho.min.css";

================================================
FILE: assets/css/common/components/_header.scss
================================================
.bd-navbar {
  height: 50px;
  padding: 0;
  background: #FFF;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
  border: 0px;
  color: $primary;
  z-index: 1030;
  line-height: 100%;
  .ml-auto {
    height: 50px;
  }
  .navbar-nav {
    padding: 0px;
    .nav-link {
      padding: 15px 15px;
      color: $primary;
    }
  }
  .navbar-topic-title {
    display: none;
  }
  &.navbar-fixed-active {
    box-shadow: 0 1px 1px rgba(0, 0, 0, .07);
  }
  .navbar-brand {
    line-height: 100%;
    color: #666 !important;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-weight: bold;
    margin-left: 1em;
    border-bottom: 0px;
    b {
      color: $grape;
    }
  }
  @include media-breakpoint-down(md) {
    padding-right: .5rem;
    padding-left: .5rem;
    .navbar-nav-scroll {
      max-width: 100%;
      overflow: hidden;
      .navbar-nav {
        overflow-x: auto;
        white-space: nowrap;
        -webkit-overflow-scrolling: touch;
      }
    }
  }
  @include media-breakpoint-up(md) {
    @supports (position: sticky) {
      position: sticky;
      top: 0;
    }
  }
  #main-nav-menu {
    &.navbar-nav .nav-item a.nav-link {
      color: $primary;
      font-size: 15px;
      border-top: 3px solid #fff;
      border-bottom: 3px solid #FFF;
      transition: all .2s ease-in-out;
    }
    &.navbar-nav .nav-item a.nav-link:active {
      background-color: $gray;
      border-bottom-color: #999;
    }
    &.navbar-nav .nav-item {
      a:hover,
      a.active,
      a.active:focus,
      a.active:hover {
        color: $grape;
        background: transparent;
        border-bottom-color: $grape;
      }
    }
  }
  .form-search {
    font-size: 14px;
    position: relative;
    padding: 15px 0;
    .fa {
      color: #333;
      &:hover {
        color: #666;
      }
    }
    .fa-search {
      cursor: pointer;
      position: absolute;
      top: 18px;
      left: 8px;
      transition: all .3s;
    }
    .form-control {
      font-size: 14px;
      border: none;
      cursor: text;
      width: 150px;
      height: 100%;
      line-height: 100%;
      padding: 6px 1px 4px 20px;
      margin-left: 4px;
      background: transparent;
      transition: all .3s;
      box-sizing: border-box;
      color: #333;
      &::-webkit-input-placeholder {
        color: #aaa;
      }
    }
  }
  .navbar-nav-svg {
    display: inline-block;
    width: 1rem;
    height: 1rem;
    vertical-align: text-top;
  }
  .dropdown.show {
    background: rgba(0, 0, 0, 0.04);
  }
  .dropdown-menu {
    border-radius: 0;
    border: 0px;
    border-left: 1px solid rgba(0, 0, 0, 0.04);
    box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.10);
    .dropdown-item {
      padding: 0.4rem 1.5rem;
    }
  }
  .dropdown-avatar {
    line-height: 100%;
    display: inline-block;
    .nav-link {
      padding: 10px 5px;
    }
    img.avatar-32 {
      width: 20px;
      height: 20px;
    }
  }
  .dropdown-item.active {
    font-weight: 500;
    color: $gray-900;
    background-color: rgba(0, 0, 0, 0.04);
  }
  .user-bar {
    background: #FFF;
    min-width: 150px;
  }
  @include media-breakpoint-down(md) {
    .user-bar {
      .nav-link {
        padding-left: 5px;
        padding-right: 5px;
      }
    }
  }
}

================================================
FILE: assets/css/common/variables/mipha.scss
================================================
$primary: #404040;
$default: #e0e0e0;
$secondary: #666;
$gray: #F2F2F2;
$red: #EB5424;
$yellow: #FEE209;
$blue: #2275da;
$grape: #9A54B1;
$green: #19A302;
$body-bg: #FFF;
$theme-colors: ( "primary": $blue, "warning": $yellow, );
$input-border: $default;
$input-color-placeholder: $default;
$link-color: $primary;
$colors: ( "blue": $blue, "grape": $grape, "red": $red, "yellow": $yellow, "green": $green, "white": #FFF, "gray": $gray, "gray-dark": $gray) !default;
$font-size-base: 0.875rem;
$spacer: 15px;
/* Animations */

.animated {
  -webkit-animation-duration: 0.5s;
  animation-duration: 0.5s;
  -webkit-animation-fill-mode: both;
  animation-fill-mode: both;
}

@-webkit-keyframes bounceIn {
  0% {
    opacity: 0;
    -webkit-transform: scale(.5);
  }
  50% {
    opacity: 1;
    -webkit-transform: scale(1.5);
  }
  70% {
    -webkit-transform: scale(.9);
  }
  100% {
    -webkit-transform: scale(1);
  }
}

@keyframes bounceIn {
  0% {
    opacity: 0;
    transform: scale(.5);
  }
  50% {
    opacity: 1;
    transform: scale(1.5);
  }
  70% {
    transform: scale(.9);
  }
  100% {
    transform: scale(1);
  }
}

.bounceIn {
  -webkit-animation-name: bounceIn;
  animation-name: bounceIn;
}

================================================
FILE: assets/js/admin.js
================================================
import 'phoenix_html'
import 'bootstrap'

import Utils from './common/components/utils'

Utils.navActive()


================================================
FILE: assets/js/app/components/editor.js
================================================
export default class Editor {
  constructor() {
    this.appendCodesFromHint()
    this.initDropzone()
    this.browseUpload()
  }

  initDropzone() {
    const self = this
    const editor = $('textarea.topic-editor')
    editor.wrap('<div class="topic-editor-dropzone"></div>')
    const editor_dropzone = $('.topic-editor-dropzone')
    editor_dropzone.on(
      'paste',
      (function(_this) {
        return function(event) {
          self.handlePaste(event)
        }
      })(this)
    )
    editor_dropzone.dropzone({
      url: '/api/callback/qiniu',
      dictDefaultMessage: '',
      clickable: true,
      paramName: 'file',
      maxFilesize: 20,
      uploadMultiple: false,
      headers: {
        'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
      },
      previewContainer: false,
      processing: function() {
        $('.div-dropzone-alert').alert('close')
        self.showUploading()
      },
      dragover: function() {
        editor.addClass('div-dropzone-focus')
      },
      dragleave: function() {
        editor.removeClass('div-dropzone-focus')
      },
      drop: function() {
        editor.removeClass('div-dropzone-focus')
        editor.focus()
      },
      success: function(header, res) {
        self.appendImageFromUpload([res.qn_url])
      },
      error: function(temp, msg) {},
      totaluploadprogress: function(num) {},
      sending: function() {},
      queuecomplete: function() {
        self.restoreUploaderStatus()
      }
    })
  }

  browseUpload() {
    $('#editor-upload-image').click(function() {
      $('.topic-editor').focus()
      $('.topic-editor-dropzone').click()
    })
  }

  uploadFile(item, filename) {
    const self = this
    const formData = new FormData()
    formData.append('file', item, filename)
    $.ajax({
      url: '/api/callback/qiniu',
      type: 'POST',
      data: formData,
      dataType: 'JSON',
      processData: false,
      contentType: false,
      beforeSend: function() {
        self.showUploading()
      },
      success: function(e, status, res) {
        self.appendImageFromUpload([res.qn_url])
        self.restoreUploaderStatus()
      },
      error: function(res) {
        self.restoreUploaderStatus()
      },
      complete: function() {
        self.restoreUploaderStatus()
      }
    })
  }

  handlePaste(e) {
    const self = this
    const pasteEvent = e.originalEvent
    if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
      const image = this.isImage(pasteEvent)
      if (image) {
        e.preventDefault()
        self.uploadFile(image.getAsFile(), 'image.png')
      }
    }
  }

  isImage(data) {
    let i
    while (i < data.clipboardData.items.length) {
      const item = data.clipboardData.items[i]
      if (item.type.indexOf('image') !== -1) {
        return item
      }
      i++
    }
  }

  showUploading() {
    $('#editor-upload-image').hide()
    if (
      $('#editor-upload-image')
        .parent()
        .find('span.loading').length === 0
    ) {
      $('#editor-upload-image').before(
        "<span class='loading'><i class='fa fa-circle-o-notch fa-spin'></i></span>"
      )
    }
  }

  appendImageFromUpload(srcs) {
    let j, len
    let src_merged = ''
    for (j = 0, len = srcs.length; j < len; j++) {
      const src = srcs[j]
      src_merged = '![](' + src + ')\n'
    }
    this.insertString(src_merged)
  }

  restoreUploaderStatus() {
    $('#editor-upload-image')
      .parent()
      .find('span.loading')
      .remove()
    $('#editor-upload-image').show()
  }

  insertString(str) {
    const $target = $('.topic-editor')
    const start = $target[0].selectionStart
    const end = $target[0].selectionEnd
    $target.val(
      $target.val().substring(0, start) + str + $target.val().substring(end)
    )
    $target[0].selectionStart = $target[0].selectionEnd = start + str.length
    $target.focus()
  }

  appendCodesFromHint() {
    $('.insert-codes a').click(function(e) {
      const link = $(e.currentTarget)
      const language = link.data('lang')
      const txtBox = $('.topic-editor')
      const caret_pos = txtBox.caret('pos')
      let prefix_break = ''
      if (txtBox.val().length > 0) {
        prefix_break = '\n'
      }
      const src_merged = prefix_break + '```' + language + '\n\n```\n'
      const source = txtBox.val()
      const before_text = source.slice(0, caret_pos)
      txtBox.val(
        before_text + src_merged + source.slice(caret_pos + 1, source.count)
      )
      txtBox.caret('pos', caret_pos + src_merged.length - 5)
      txtBox.focus()
      txtBox.trigger('click')
    })
  }
}


================================================
FILE: assets/js/app/components/session.js
================================================
export default class Session {
  static refreshExcaptcha() {
    $('a.excaptcha-image-box').click(function(e) {
      const img = $(e.currentTarget).find('img:first')
      const currentSrc = img.attr('src')
      img.attr('src', currentSrc.split('?')[0] + '?' + new Date().getTime())
    })
  }
}


================================================
FILE: assets/js/app/components/times.js
================================================
const moment = require('moment')
moment.locale('zh-cn', {
  months: '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split(
    '_'
  ),
  monthsShort: '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'),
  weekdays: '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'),
  weekdaysShort: '周日_周一_周二_周三_周四_周五_周六'.split('_'),
  weekdaysMin: '日_一_二_三_四_五_六'.split('_'),
  longDateFormat: {
    LT: 'Ah点mm分',
    LTS: 'Ah点m分s秒',
    L: 'YYYY-MM-DD',
    LL: 'YYYY年MMMD日',
    LLL: 'YYYY年MMMD日Ah点mm分',
    LLLL: 'YYYY年MMMD日ddddAh点mm分',
    l: 'YYYY-MM-DD',
    ll: 'YYYY年MMMD日',
    lll: 'YYYY年MMMD日Ah点mm分',
    llll: 'YYYY年MMMD日ddddAh点mm分'
  },
  meridiemParse: /凌晨|早上|上午|中午|下午|晚上/,
  meridiemHour: function(h, meridiem) {
    let hour = h
    if (hour === 12) {
      hour = 0
    }
    if (meridiem === '凌晨' || meridiem === '早上' || meridiem === '上午') {
      return hour
    } else if (meridiem === '下午' || meridiem === '晚上') {
      return hour + 12
    } else {
      // '中午'
      return hour >= 11 ? hour : hour + 12
    }
  },
  meridiem: function(hour, minute, isLower) {
    const hm = hour * 100 + minute
    if (hm < 600) {
      return '凌晨'
    } else if (hm < 900) {
      return '早上'
    } else if (hm < 1130) {
      return '上午'
    } else if (hm < 1230) {
      return '中午'
    } else if (hm < 1800) {
      return '下午'
    } else {
      return '晚上'
    }
  },
  calendar: {
    sameDay: function() {
      return this.minutes() === 0 ? '[今天]Ah[点整]' : '[今天]LT'
    },
    nextDay: function() {
      return this.minutes() === 0 ? '[明天]Ah[点整]' : '[明天]LT'
    },
    lastDay: function() {
      return this.minutes() === 0 ? '[昨天]Ah[点整]' : '[昨天]LT'
    },
    nextWeek: function() {
      let startOfWeek, prefix
      startOfWeek = moment().startOf('week')
      prefix = this.diff(startOfWeek, 'days') >= 7 ? '[下]' : '[本]'
      return this.minutes() === 0 ? prefix + 'dddAh点整' : prefix + 'dddAh点mm'
    },
    lastWeek: function() {
      let startOfWeek, prefix
      startOfWeek = moment().startOf('week')
      prefix = this.unix() < startOfWeek.unix() ? '[上]' : '[本]'
      return this.minutes() === 0 ? prefix + 'dddAh点整' : prefix + 'dddAh点mm'
    },
    sameElse: 'LL'
  },
  ordinalParse: /\d{1,2}(日|月|周)/,
  ordinal: function(number, period) {
    switch (period) {
      case 'd':
      case 'D':
      case 'DDD':
        return number + '日'
      case 'M':
        return number + '月'
      case 'w':
      case 'W':
        return number + '周'
      default:
        return number
    }
  },
  relativeTime: {
    future: '%s内',
    past: '%s前',
    s: '几秒',
    m: '1 分钟',
    mm: '%d 分钟',
    h: '1 小时',
    hh: '%d 小时',
    d: '1 天',
    dd: '%d 天',
    M: '1 个月',
    MM: '%d 个月',
    y: '1 年',
    yy: '%d 年'
  },
  week: {
    // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效
    dow: 1, // Monday is the first day of the week.
    doy: 4 // The week that contains Jan 4th is the first week of the year.
  }
})

export default class Times {
  static humanize() {
    $('addr.timeago').html((_, html) => {
      return moment.utc(html.trim(), moment.ISO_8601).fromNow()
    })
  }
}


================================================
FILE: assets/js/app/components/topic.js
================================================
import 'jquery'
import 'jquery.caret'
import _ from 'lodash'
import 'static/atwho/jquery.atwho.min'

const selectorNode = () => {
  $('#node-selector .nodes .name a').click(function(e) {
    const el = $(e.currentTarget)
    $('#node-selector').modal('hide')
    $('form input[name="topic[title]"]').focus()
    if ($('form input[name="topic[node_id]"]').length > 0) {
      e.preventDefault()
      const nodeId = el.data('id')
      $('form input[name="topic[node_id]"]').val(nodeId)
      $('#node-selector-button').html(el.text())
      return false
    } else {
      return true
    }
  })
}

const hookPreview = (switcher, textarea) => {
  const preview_box = $(document.createElement('div')).attr('id', 'preview')
  preview_box.addClass('markdown form-control')
  $(textarea).after(preview_box)
  preview_box.hide()
  $('.preview', switcher).click(function() {
    if ($(this).hasClass('active')) {
      $(this).removeClass('active')
      $(preview_box).hide()
      $(textarea).show()
    } else {
      $(this).addClass('active')
      $(preview_box).show()
      $(textarea).hide()
      $(preview_box).css('height', $(textarea).height())
      preview($(textarea).val())
    }
  })
}

const preview = body => {
  $('#preview').text('Loading...')
  $.post(
    '/api/topics/preview',
    {
      body: body
    },
    function(data) {
      return $('#preview').html(data.body)
    },
    'json'
  )
}

const hookReply = () => {
  $('.btn-reply').click(function() {
    const replyId = $(this).data('id')
    setReplyTo(replyId)
    const reply_body = $('#new_reply textarea')
    reply_body.focus()
  })
}

const setReplyTo = id => {
  $('input[name="reply[parent_id]"]').val(id)
  const replyEl = $(`.reply[data-id=${id}]`)
  const targetAnchor = replyEl.attr('id')
  const replyToPanel = $('.editor-toolbar .reply-to')
  const userNameEl = replyEl.find('a.user-name:first-child')
  const replyToLink = replyToPanel.find('.user')

  replyToLink.attr('href', `#${targetAnchor}`)
  replyToLink.text(userNameEl.text())
  replyToPanel.show()
}

const hookMention = () => {
  $('textarea').atwho({
    at: '@',
    limit: 8,
    searchKey: 'login',
    callbacks: {
      filter: function(query, data, searchKey) {
        return data
      },
      sorter: function(query, items, searchKey) {
        return items
      },
      remoteFilter: function(query, callback) {
        $.getJSON(
          '/search/users',
          {
            q: query
          },
          function(data) {
            callback(data)
          }
        )
      }
    },
    displayTpl:
      "<li data-value='${login}'><img src='${avatar_url}' height='20' width='20'/> ${login} </li>",
    insertTpl: '@${login}'
  })
}

const reloadWhenNewReply = () => {
  $('.notify-updated a').click(function() {
    window.location.reload()
    $('.notify-updated').hide()
  })
}

const Topic = {
  selectorNode,
  hookPreview,
  hookReply,
  hookMention,
  reloadWhenNewReply
}

export default Topic


================================================
FILE: assets/js/app.js
================================================
import 'phoenix_html'
import 'bootstrap'
import 'jquery.caret'
import 'dropzone/dist/dropzone-amd-module'

import socket from './socket'

// JS components
import Times from './app/components/times'
import Utils from './common/components/utils'
import Topic from './app/components/topic'
import Editor from './app/components/editor'
import Session from './app/components/session'

// Decorate
Times.humanize()
Utils.navActive()
// Topic
Topic.selectorNode()
Topic.hookPreview($('.editor-toolbar'), $('.topic-editor'))
Topic.hookReply()
Topic.hookMention()
Topic.reloadWhenNewReply()
// Editor
window._editor = new Editor()
// Refresh Captcha
Session.refreshExcaptcha()


================================================
FILE: assets/js/common/components/utils.js
================================================
export default class Utils {
  static navActive() {
    const url =
      window.location.protocol +
      '//' +
      window.location.host +
      window.location.pathname

    $('.mi-nav li a')
      .filter(function() {
        return this.href == url
      })
      .closest('a')
      .addClass('active')
  }
}


================================================
FILE: assets/js/socket.js
================================================
// NOTE: The contents of this file will only be executed if
// you uncomment its entry in "assets/js/app.js".

// To use Phoenix channels, the first step is to import Socket
// and connect at the socket path in "lib/web/endpoint.ex":
import { Socket, Presence } from 'phoenix'

let socket = new Socket('/socket', { params: { token: window.userToken } })

socket.connect()

// Now that you are connected, you can join channels with a topic:
let channel = socket.channel('room:lobby', {})
channel
  .join()
  .receive('ok', resp => {
    console.log('Joined successfully', resp)
  })
  .receive('error', resp => {
    console.log('Unable to join', resp)
  })

// Presences
let presences = {}
channel.on('presence_state', state => {
  presences = Presence.syncState(presences, state)
  renderOnlineUsers(presences)
})

channel.on('presence_diff', diff => {
  presences = Presence.syncDiff(presences, diff)
  renderOnlineUsers(presences)
})

const renderOnlineUsers = function(presences) {
  let onlineUsers = `<strong>${
    Object.keys(presences).length
  } Online Users</strong>`
  document.querySelector('#online-users').innerHTML = onlineUsers
}

let channelTopicId = window.channelTopicId

if (channelTopicId) {
  let channel = socket.channel(`topic:${channelTopicId}`, {})
  channel
    .join()
    .receive('ok', resp => {
      console.log('Joined topic successfully', resp)
    })
    .receive('error', resp => {
      console.log('Unable to join', resp)
    })

  channel.on(`topic:${channelTopicId}:new_reply`, message => {
    if (message) {
      $('.notify-updated').show()
    }
  })
}

export default socket


================================================
FILE: assets/package.json
================================================
{
  "repository": {},
  "license": "MIT",
  "engines": {
    "node": ">=9.0",
    "yarn": ">=1.6.0"
  },
  "scripts": {
    "deploy": "webpack --mode production",
    "watch": "webpack --mode development --watch",
    "lint": "eslint --ext .js js",
    "prettier": "prettier 'js/**/*js' --write",
    "precommit": "lint-staged"
  },
  "prettier": {
    "printWidth": 80,
    "semi": false,
    "singleQuote": true,
    "trailingComma": "none",
    "bracketSpacing": true
  },
  "lint-staged": {
    "js/**/*.js": "eslint",
    "*.{js,json,css,md}": [
      "prettier --write",
      "git add"
    ]
  },
  "dependencies": {
    "bootstrap": "^4.3.1",
    "dropzone": "^5.5.1",
    "font-awesome": "^4.7.0",
    "jquery": "^3.4.1",
    "jquery.caret": "^0.3.1",
    "lodash": "^4.17.13",
    "moment": "^2.22.2",
    "phoenix": "file:../deps/phoenix",
    "phoenix_html": "file:../deps/phoenix_html",
    "popper.js": "^1.14.3"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-eslint": "^10.0.2",
    "babel-loader": "^7.1.3",
    "babel-preset-env": "^1.6.1",
    "copy-webpack-plugin": "^5.1.1",
    "css-loader": "^3.0.0",
    "eslint": "^6.8.0",
    "eslint-config-prettier": "^2.9.0",
    "file-loader": "^1.1.11",
    "husky": "^0.14.3",
    "lint-staged": "^7.2.0",
    "mini-css-extract-plugin": "^0.4.0",
    "node-sass": "^4.12.0",
    "optimize-css-assets-webpack-plugin": "^5.0.1",
    "prettier": "^1.13.7",
    "sass-loader": "^7.0.1",
    "style-loader": "^0.20.2",
    "uglifyjs-webpack-plugin": "^1.2.4",
    "webpack": "^4.42.0",
    "webpack-cli": "^3.3.4"
  }
}


================================================
FILE: assets/static/robots.txt
================================================
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /


================================================
FILE: assets/webpack.config.js
================================================
const path = require('path')
const webpack = require('webpack')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const assets = path.normalize(__dirname)

module.exports = (env, options) => ({
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourceMap: false
      }),
      new OptimizeCSSAssetsPlugin({})
    ]
  },
  entry: {
    app: ['./js/app.js', './css/app.scss'],
    admin: ['./js/admin.js', './css/admin.scss']
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, '../priv/static/js')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.s?[ac]ss$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      },
      {
        test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
              outputPath: '../css/fonts/' // where the fonts will go
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '../css/[name].css'
    }),
    new CopyWebpackPlugin([
      {
        from: 'static/',
        to: '../'
      }
    ]),
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      'window.jQuery': 'jquery'
    })
  ],
  resolve: {
    alias: {
      static: `${assets}/static`
    }
  }
})


================================================
FILE: compile
================================================
cd $phoenix_dir
npm --prefix ./assets run deploy
mix "${phoenix_ex}.digest"


================================================
FILE: config/config.exs
================================================
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config

# General application configuration
config :mipha,
  ecto_repos: [Mipha.Repo]

# Configures the endpoint
config :mipha, MiphaWeb.Endpoint,
  url: [host: "localhost"],
  secret_key_base: "Gbwl1EqAtjdYfG5movSWszQHvrppYxDbg/7xegRJSakWiTTl6ypdMpNgNct3LiDx",
  render_errors: [view: MiphaWeb.ErrorView, accepts: ~w(html json)],
  pubsub_server: Mipha.PubSub
  # pubsub: [name: Mipha.PubSub,
  #          adapter: Phoenix.PubSub.PG2]

# Configures Elixir's Logger
config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:user_id]

config :ueberauth, Ueberauth,
  providers: [
    github: {Ueberauth.Strategy.Github, [default_scope: "user:email", allow_private_emails: true]},
    identity: {Ueberauth.Strategy.Identity, [callback_methods: ["POST"]]},
  ]

config :ueberauth, Ueberauth.Strategy.Github.OAuth,
  client_id: System.get_env("GITHUB_CLIENT_ID"),
  client_secret: System.get_env("GITHUB_CLIENT_SECRET")

config :qiniu, Qiniu,
  access_key: System.get_env("QINIU_ACCESS_KEY"),
  secret_key: System.get_env("QINIU_SECRET_KEY")

config :mipha, Mipha.Mailer,
  adapter: Bamboo.LocalAdapter

# Sentry
config :sentry,
  dsn: System.get_env("SENRTY_DSN"),
  included_environments: [:prod],
  environment_name: Mix.env(),
  enable_source_code_context: true,
  root_source_code_path: File.cwd!()

config :turbo_ecto, Turbo.Ecto,
  repo: Mipha.Repo


config :phoenix, :json_library, Jason

config :gettext, :default_locale, "zh"

# git_hooks
if Mix.env() != :prod do
  config :git_hooks,
    auto_install: true,
    verbose: true,
    hooks: [
      pre_commit: [
        tasks: [
          {:cmd, "mix format"},
          {:cmd, "mix credo --strict"}
        ]
      ],
      pre_push: [
        verbose: false,
        tasks: [
          {:cmd, "mix test"},
          {:cmd, "echo 'success!'"}
        ]
      ]
    ]
end

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"


================================================
FILE: config/dev.exs
================================================
use Mix.Config

# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources.
config :mipha, MiphaWeb.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [node: ["node_modules/webpack/bin/webpack.js", "--mode", "development", "--watch-stdin",
                      cd: Path.expand("../assets", __DIR__)]]

# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# command from your terminal:
#
#     openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem
#
# The `http:` config above can be replaced with:
#
#     https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"],
#
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.

# Watch static and templates for browser reloading.
config :mipha, MiphaWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
      ~r{priv/gettext/.*(po)$},
      ~r{lib/mipha_web/views/.*(ex)$},
      ~r{lib/mipha_web/templates/.*(eex)$}
    ]
  ]

# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"

# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20

# Configure your database
config :mipha, Mipha.Repo,
  username: "postgres",
  password: "postgres",
  database: "mipha_dev",
  hostname: "localhost",
  pool_size: 10


================================================
FILE: config/prod.exs
================================================
use Mix.Config

# For production, we often load configuration from external
# sources, such as your system environment. For this reason,
# you won't find the :http configuration below, but set inside
# MiphaWeb.Endpoint.init/2 when load_from_system_env is
# true. Any dynamic configuration should be done there.
#
# Don't forget to configure the url host to something meaningful,
# Phoenix uses this information when generating URLs.
#
# Finally, we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the mix phx.digest task
# which you typically run after static files are built.
config :mipha, MiphaWeb.Endpoint,
  load_from_system_env: true,
  url: [scheme: "https", host: "elixir-mipha.herokuapp.com", port: 443],
  cache_static_manifest: "priv/static/cache_manifest.json",
  secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE")

# Do not print debug messages in production
config :logger, level: :info

config :mipha, Mipha.Mailer,
  adapter: Bamboo.SMTPAdapter,
  port: 465,
  server: "smtp.gmail.com",
  username: System.get_env("GMAIL_USERNAME"),
  password: System.get_env("GMAIL_PASSWORD"),
  tls: :never, # can be `:always` or `:never` or `:if_available`
  ssl: true,
  retries: 3

# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to the previous section and set your `:url` port to 443:
#
#     config :mipha, MiphaWeb.Endpoint,
#       ...
#       url: [host: "example.com", port: 443],
#       https: [:inet6,
#               port: 443,
#               keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
#               certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
#
# Where those two env variables return an absolute path to
# the key and cert in disk or a relative path inside priv,
# for example "priv/ssl/server.key".
#
# We also recommend setting `force_ssl`, ensuring no data is
# ever sent via http, always redirecting to https:
#
#     config :mipha, MiphaWeb.Endpoint,
#       force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.

# ## Using releases
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start the server for all endpoints:
#
#     config :phoenix, :serve_endpoints, true
#
# Alternatively, you can configure exactly which server to
# start per endpoint:
#
#     config :mipha, MiphaWeb.Endpoint, server: true
#

# Finally import the config/prod.secret.exs
# which should be versioned separately.
# import_config "prod.secret.exs"
config :mipha, Mipha.Repo,
  adapter: Ecto.Adapters.Postgres,
  url: System.get_env("DATABASE_URL"),
  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
  ssl: true


================================================
FILE: config/test.exs
================================================
use Mix.Config

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :mipha, MiphaWeb.Endpoint,
  http: [port: 4001],
  server: false

# Print only warnings and errors during test
config :logger, level: :warn

# Configure your database
config :mipha, Mipha.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "postgres",
  password: "postgres",
  database: "mipha_test",
  hostname: System.get_env("DB_HOST") || "localhost",
  pool: Ecto.Adapters.SQL.Sandbox


================================================
FILE: docker/docker-compose.yml
================================================
version: "3.6"

# cd docker && docker-compose up -d

services:
  postgresql_dev:
    image: sameersbn/postgresql:9.6
    environment:
      - PG_PASSWORD=postgres
    ports:
      - "5432:5432"


================================================
FILE: elixir_buildpack.config
================================================
# Elixir version
elixir_version=1.8.1

# Erlang version
# available versions https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions
erlang_version=21.2.5

================================================
FILE: lib/mipha/accounts/accounts.ex
================================================
defmodule Mipha.Accounts do
  @moduledoc """
  The Accounts context.
  """

  import Ecto.Query, warn: false

  alias Comeonin.Bcrypt
  alias HTTPoison
  alias Mipha.Repo
  alias Mipha.Accounts.{User, Team}
  alias Mipha.Utils.Store
  alias Mipha.Follows.Follow

  @doc """
  Gets a single user.

  Raises `Ecto.NoResultsError` if the User does not exist.

  ## Examples

      iex> get_user!(123)
      %User{}

      iex> get_user!(456)
      ** (Ecto.NoResultsError)

  """
  def get_user!(id), do: Repo.get!(User, id)

  def get_user(id), do: Repo.get(User, id)

  @doc """
  Creates a user.

  ## Examples

      iex> create_user(%{field: value})
      {:ok, %User{}}

      iex> create_user(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a user.

  ## Examples

      iex> update_user(user, %{field: new_value})
      {:ok, %User{}}

      iex> update_user(user, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a User.

  ## Examples

      iex> delete_user(user)
      {:ok, %User{}}

      iex> delete_user(user)
      {:error, %Ecto.Changeset{}}

  """
  def delete_user(%User{} = user) do
    Repo.delete(user)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking user changes.

  ## Examples

      iex> change_user(user)
      %Ecto.Changeset{source: %User{}}

  """
  def change_user(%User{} = user) do
    User.changeset(user, %{})
  end

  @doc """
  Register user.

  ## Example

      iex> register_user(%{username: "user", email:"user@minpha.com", password: "123123123"})
      {:ok, %User{}}

      iex> register_user(%{username: "user", email:"user@minpha", password: "123123123"})
      {:error, %Ecto.Changeset{}}

  """
  @spec register_user(map()) :: {:ok, User.t()} | {:error, %Ecto.Changeset{}}
  def register_user(attrs) do
    %User{}
    |> User.register_changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Authenticate user login.

  ## Examples

      iex> authenticate(%{login: "zven", password: "123123123"})
      {:ok, %User{}}

      iex> authenticate(%{login: "zven", password: "123123123"})
      {:error, "Incorrect login credentials"}

  """
  @spec authenticate(map()) :: {:ok, User.t()} | {:error, String.t()}
  def authenticate(attrs) do
    user = get_user_by_email(attrs.login) || get_user_by_username(attrs.login)

    case check_user_password(user, attrs.password) do
      true -> {:ok, user}
      _ -> {:error, "Failed auth."}
    end
  end

  @doc """
  Gets a user by given clauses.
  """
  @spec get_user_by(Keyword.t(), Keyword.t()) :: User.t() | nil
  def get_user_by(clauses, _opts \\ []) do
    User
    |> preload([:location, :company, :teams])
    |> Repo.get_by(clauses)
  end

  @doc """
  Gets a user by its username.

  ## Examples

      iex> get_user_by_username("user123")
      %User{}

      iex> get_user_by_username("user456")
      nil

  """
  @spec get_user_by_username(String.t(), Keyword.t()) :: User.t() | nil
  def get_user_by_username(username, opts \\ []),
    do: get_user_by([username: username], opts)

  @doc """
  Gets a user by its email.

  ## Examples

      iex> get_user_by_email("user123@mipha.com")
      %User{}

      iex> get_user_by_email("user456@mipha.com")
      nil

  """
  @spec get_user_by_email(String.t(), Keyword.t()) :: User.t() | nil
  def get_user_by_email(email, opts \\ []),
    do: get_user_by([email: email], opts)

  defp check_user_password(user, password) do
    case user do
      nil -> false
      _ -> !is_nil(user.password_hash) && Bcrypt.checkpw(password, user.password_hash)
    end
  end

  @doc """
  Gets the user count.

  ## Examples

      iex> get_user_count
      40

  """
  @spec get_user_count :: non_neg_integer()
  def get_user_count do
    User
    |> Repo.aggregate(:count, :id)
  end

  @doc """
  获取全部 user 个数
  """
  @spec get_total_user_count :: non_neg_integer()
  def get_total_user_count do
    User
    |> Repo.aggregate(:count, :id)
  end

  @doc """
  Updates user password.
  """
  @spec update_user_password(User.t(), map()) :: {:ok, User.t()} | {:error, any()}
  def update_user_password(user, attrs) do
    case check_user_password(user, attrs["current_password"]) do
      true ->
        user
        |> User.update_password_changeset(attrs)
        |> Repo.update()

      _ ->
        {:error, "Invalid current password"}
    end
  end

  @doc """
  Mark the current user verified
  """
  def mark_as_verified(user) do
    attrs = %{"email_verified_at" => Timex.now()}
    update_user(user, attrs)
  end

  @doc """
  找回密码-重置密码
  """
  def update_reset_password(user, attrs) do
    user
    |> User.reset_password_changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Github 登录应对。
  """
  def login_or_register_from_github(%{nickname: nickname, name: nil, email: _email} = user) do
    login_or_register_from_github(%{user | name: nickname})
  end

  def login_or_register_from_github(%{nickname: nickname, name: _name, email: nil} = user) do
    login_or_register_from_github(%{user | email: nickname <> "@users.noreply.github.com"})
  end

  def login_or_register_from_github(%{nickname: nickname, name: _name, email: email}) do
    case get_user_by_username(nickname) do
      nil ->
        create_user(%{
          email: email,
          username: nickname
        })

      user ->
        {:ok, user}
    end
  end

  @doc """
  获取 用户或组织的 github repos,这里会借助缓存处理。

  ## TODO

      需要队列处理

  ## Example

      iex> github_repositories(user)
      [%{}, %{}]

      iex> github_repositories(user)
      []

  """
  def github_repositories(%User{} = user) do
    user
    |> github_repos_cache_key
    |> Store.get!()
    |> fetch_github_repos(user)
  end

  def github_repositories(%Team{} = team) do
    team
    |> github_repos_cache_key
    |> Store.get!()
    |> fetch_github_repos(team)
  end

  # 获取 github repos 信息
  defp fetch_github_repos(items, target) when is_nil(items) do
    repos =
      target
      |> github_repos_url
      |> HTTPoison.get!()
      |> handle_response

    Store.put!(github_repos_cache_key(target), repos)
    repos
  end

  defp fetch_github_repos(items, _) do
    items
  end

  # 拉取数据,并且 Json 处理
  defp handle_response(%HTTPoison.Response{body: body, status_code: 200}) do
    body
    |> Jason.decode!()
    |> Enum.map(&Map.take(&1, ~w(name html_url watchers language description)))
    |> Enum.sort(&(&1["watchers"] >= &2["watchers"]))
    |> Enum.take(10)
  end

  defp handle_response(%HTTPoison.Response{body: _, status_code: 404}) do
    []
  end

  # 获取缓存 cache_key
  defp github_repos_cache_key(target) do
    "github-repos:" <> github_handle(target)
  end

  # 请求获取 github 用户的 repos 的 Url
  defp github_repos_url(target) do
    "https://api.github.com/users/#{github_handle(target)}/repos?type=owner&sort=pushed&client_id=#{
      System.get_env("GITHUB_CLIENT_ID")
    }&client_secret=#{System.get_env("GITHUB_CLIENT_SECRET")}"
  end

  @doc """
  获取 user 或 team 的 github 账号。
  """
  def github_handle(%User{} = user) do
    user.github_handle || user.username
  end

  def github_handle(%Team{} = team) do
    team.github_handle || team.name
  end

  @doc """
  添加 帖子与评论内容时,mention user.
  """
  @spec search_mention_user(User.t(), String.t()) :: any
  def search_mention_user(%User{} = user, q) do
    query =
      from u in User,
        join: f in Follow,
        on: f.user_id == u.id,
        where: f.follower_id == ^user.id,
        where: like(u.username, ^"#{q}%")

    query
    |> Repo.all()
    |> Enum.uniq()
  end

  @doc """
  Returns the user register changeset.
  """
  @spec user_register_changeset(Map.t()) :: Ecto.Changeset.t()
  def user_register_changeset(attrs \\ %{}), do: User.register_changeset(%User{}, attrs)

  @doc """
  Returns the user login changeset.
  """
  @spec user_login_changeset(Map.t()) :: Ecto.Changeset.t()
  def user_login_changeset(attrs \\ %{}), do: User.login_changeset(%User{}, attrs)

  @doc """
  Returns the user update_password changeset
  """
  @spec user_update_password_changeset(Map.t()) :: Ecto.Changeset.t()
  def user_update_password_changeset(attrs \\ %{}),
    do: User.update_password_changeset(%User{}, attrs)

  @doc """
  Returns the change user reset password.
  """
  @spec change_user_reset_password(User.t(), Map.t()) :: Ecto.Changeset.t()
  def change_user_reset_password(%User{} = user, attrs \\ %{}),
    do: User.reset_password_changeset(user, attrs)

  alias Mipha.Accounts.Location

  @doc """
  Returns the list of locations.

  ## Examples

      iex> list_locations()
      [%Location{}, ...]

  """
  def list_locations do
    Repo.all(Location)
  end

  @doc """
  Gets a single location.

  Raises `Ecto.NoResultsError` if the Location does not exist.

  ## Examples

      iex> get_location!(123)
      %Location{}

      iex> get_location!(456)
      ** (Ecto.NoResultsError)

  """
  def get_location!(id) do
    Location
    |> Repo.get!(id)
    |> Repo.preload([:users])
  end

  @doc """
  Creates a location.

  ## Examples

      iex> create_location(%{field: value})
      {:ok, %Location{}}

      iex> create_location(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_location(attrs \\ %{}) do
    %Location{}
    |> Location.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a location.

  ## Examples

      iex> update_location(location, %{field: new_value})
      {:ok, %Location{}}

      iex> update_location(location, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_location(%Location{} = location, attrs) do
    location
    |> Location.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Location.

  ## Examples

      iex> delete_location(location)
      {:ok, %Location{}}

      iex> delete_location(location)
      {:error, %Ecto.Changeset{}}

  """
  def delete_location(%Location{} = location) do
    Repo.delete(location)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking location changes.

  ## Examples

      iex> change_location(location)
      %Ecto.Changeset{source: %Location{}}

  """
  def change_location(%Location{} = location) do
    Location.changeset(location, %{})
  end

  alias Mipha.Accounts.Company

  @doc """
  Gets a single company.

  Raises `Ecto.NoResultsError` if the Company does not exist.

  ## Examples

      iex> get_company!(123)
      %Company{}

      iex> get_company!(456)
      ** (Ecto.NoResultsError)

  """
  def get_company!(id), do: Repo.get!(Company, id)

  @doc """
  Creates a company.

  ## Examples

      iex> create_company(%{field: value})
      {:ok, %Company{}}

      iex> create_company(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_company(attrs \\ %{}) do
    %Company{}
    |> Company.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a company.

  ## Examples

      iex> update_company(company, %{field: new_value})
      {:ok, %Company{}}

      iex> update_company(company, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_company(%Company{} = company, attrs) do
    company
    |> Company.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Company.

  ## Examples

      iex> delete_company(company)
      {:ok, %Company{}}

      iex> delete_company(company)
      {:error, %Ecto.Changeset{}}

  """
  def delete_company(%Company{} = company) do
    Repo.delete(company)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking company changes.

  ## Examples

      iex> change_company(company)
      %Ecto.Changeset{source: %Company{}}

  """
  def change_company(%Company{} = company) do
    Company.changeset(company, %{})
  end

  @doc """
  Returns the list of teams.

  ## Examples

      iex> list_teams()
      [%Team{}, ...]

  """
  def list_teams do
    Repo.all(Team)
  end

  @doc """
  Gets a single team.

  Raises `Ecto.NoResultsError` if the Team does not exist.

  ## Examples

      iex> get_team!(123)
      %Team{}

      iex> get_team!(456)
      ** (Ecto.NoResultsError)

  """
  def get_team!(id) do
    Team
    |> Repo.get!(id)
    |> Repo.preload([:users, :owner])
  end

  @doc """
  Creates a team.

  ## Examples

      iex> create_team(%{field: value})
      {:ok, %Team{}}

      iex> create_team(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_team(attrs \\ %{}) do
    %Team{}
    |> Team.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a team.

  ## Examples

      iex> update_team(team, %{field: new_value})
      {:ok, %Team{}}

      iex> update_team(team, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_team(%Team{} = team, attrs) do
    team
    |> Team.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Team.

  ## Examples

      iex> delete_team(team)
      {:ok, %Team{}}

      iex> delete_team(team)
      {:error, %Ecto.Changeset{}}

  """
  def delete_team(%Team{} = team) do
    Repo.delete(team)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking team changes.

  ## Examples

      iex> change_team(team)
      %Ecto.Changeset{source: %Team{}}

  """
  def change_team(%Team{} = team) do
    Team.changeset(team, %{})
  end

  alias Mipha.Accounts.UserTeam

  @doc """
  Returns the list of users_teams.

  ## Examples

      iex> list_users_teams()
      [%UserTeam{}, ...]

  """
  def list_users_teams do
    Repo.all(UserTeam)
  end

  @doc """
  Gets a single user_team.

  Raises `Ecto.NoResultsError` if the User team does not exist.

  ## Examples

      iex> get_user_team!(123)
      %UserTeam{}

      iex> get_user_team!(456)
      ** (Ecto.NoResultsError)

  """
  def get_user_team!(id), do: Repo.get!(UserTeam, id)

  @doc """
  Creates a user_team.

  ## Examples

      iex> create_user_team(%{field: value})
      {:ok, %UserTeam{}}

      iex> create_user_team(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_user_team(attrs \\ %{}) do
    %UserTeam{}
    |> UserTeam.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a user_team.

  ## Examples

      iex> update_user_team(user_team, %{field: new_value})
      {:ok, %UserTeam{}}

      iex> update_user_team(user_team, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_user_team(%UserTeam{} = user_team, attrs) do
    user_team
    |> UserTeam.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a UserTeam.

  ## Examples

      iex> delete_user_team(user_team)
      {:ok, %UserTeam{}}

      iex> delete_user_team(user_team)
      {:error, %Ecto.Changeset{}}

  """
  def delete_user_team(%UserTeam{} = user_team) do
    Repo.delete(user_team)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking user_team changes.

  ## Examples

      iex> change_user_team(user_team)
      %Ecto.Changeset{source: %UserTeam{}}

  """
  def change_user_team(%UserTeam{} = user_team) do
    UserTeam.changeset(user_team, %{})
  end
end


================================================
FILE: lib/mipha/accounts/company.ex
================================================
defmodule Mipha.Accounts.Company do
  @moduledoc false

  use Ecto.Schema
  import Ecto.Changeset
  alias Mipha.Accounts.{User, Location, Company}

  @type t :: %Company{}

  schema "companies" do
    field :name, :string
    field :avatar, :string

    belongs_to :location, Location

    has_many :users, User

    timestamps()
  end

  @doc false
  def changeset(company, attrs) do
    permitted_attrs = ~w(
      name
      avatar
      location_id
    )a

    required_attrs = ~w(
      name
      location_id
    )a

    company
    |> cast(attrs, permitted_attrs)
    |> validate_required(required_attrs)
  end
end


================================================
FILE: lib/mipha/accounts/location.ex
================================================
defmodule Mipha.Accounts.Location do
  @moduledoc false

  use Ecto.Schema
  import Ecto.Changeset

  alias Mipha.Accounts.{User, Location, Company}

  @type t :: %Location{}

  schema "locations" do
    field :name, :string

    has_many :users, User
    has_many :companies, Company

    timestamps()
  end

  @doc false
  def changeset(location, attrs) do
    location
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end
end


================================================
FILE: lib/mipha/accounts/queries.ex
================================================
defmodule Mipha.Accounts.Queries do
  @moduledoc false

  alias Mipha.Accounts.{User, Team, Company}
  import Ecto.Query

  @doc """
  Returns the list users.
  """
  @spec list_users :: Ecto.Query.t()
  def list_users, do: User |> order_by([q], asc: q.inserted_at)

  @doc """
  Returns the list teams.
  """
  @spec list_teams :: Ecto.Query.t()
  def list_teams, do: Team

  @doc """
  Returns the list companies.
  """
  @spec list_companies :: Ecto.Query.t()
  def list_companies, do: Company
end


================================================
FILE: lib/mipha/accounts/team.ex
================================================
defmodule Mipha.Accounts.Team do
  @moduledoc false

  use Ecto.Schema
  import Ecto.Changeset
  alias Mipha.Accounts.{User, Team, UserTeam}

  @type t :: %Team{}

  schema "teams" do
    field :name, :string
    field :slug, :string
    field :avatar, :string
    field :summary, :string
    field :website, :string
    field :github_handle, :string
    field :twitter_handle, :string
    field :email, :string
    field :email_public, :boolean

    belongs_to :owner, User

    many_to_many :users, User, join_through: UserTeam

    timestamps()
  end

  @doc false
  def changeset(team, attrs) do
    team
    |> cast(attrs, [:owner_id, :github_handle, :name, :summary, :avatar])
    |> validate_required([:owner_id, :github_handle, :name, :summary, :avatar])
  end
end


================================================
FILE: lib/mipha/accounts/user.ex
================================================
defmodule Mipha.Accounts.User do
  @moduledoc false

  use Ecto.Schema
  import Ecto.Changeset
  alias Comeonin.Bcrypt

  alias Mipha.{
    Regexp,
    Topics.Topic,
    Replies.Reply,
    Follows.Follow,
    Stars.Star,
    Collections.Collection
  }

  alias Mipha.Notifications.{Notification, UserNotification}
  alias Mipha.Accounts.{User, Location, Company, Team, UserTeam}

  @type t :: %User{}

  schema "users" do
    field :username, :string
    field :avatar, :string
    field :bio, :string
    field :email, :string
    field :email_public, :boolean, default: false
    field :email_verified_at, :naive_datetime
    field :tagline, :string
    field :github_handle, :string
    field :is_admin, :boolean, default: false
    field :password_hash, :string, default: ""
    field :website, :string
    field :alipay, :string
    field :wechat, :string
    field :locked_at, :naive_datetime
    field :password, :string, virtual: true
    field :password_confirmation, :string, virtual: true
    field :login, :string, virtual: true
    field :reset_password_token, :string, virtual: true

    belongs_to :location, Location
    belongs_to :company, Company

    has_many :topics, Topic, on_delete: :delete_all
    has_many :replies, Reply, on_delete: :delete_all
    has_many :followers, Follow, on_delete: :delete_all
    has_many :following, Follow, foreign_key: :follower_id
    has_many :stars, Star, on_delete: :delete_all
    has_many :collections, Collection, on_delete: :delete_all

    many_to_many :teams, Team, join_through: UserTeam
    many_to_many :notifications, Notification, join_through: UserNotification

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    permitted_attrs = ~w(
      username
      email
      avatar
      bio
      website
      github_handle
      is_admin
      location_id
      company_id
      tagline
      email_public
      wechat
      alipay
      email_verified_at
    )a

    required_attrs = ~w(
      username
      email
    )a

    user
    |> cast(attrs, permitted_attrs)
    |> validate_required(required_attrs)
    |> assoc_constraint(:location)
    |> assoc_constraint(:company)
  end

  @doc """
  User login changeset.
  """
  def login_changeset(user, attrs) do
    login_attrs = ~w(
      login
      password
    )a

    user
    |> cast(attrs, login_attrs)
    |> validate_required(login_attrs)
  end

  @doc """
  Registration changeset.
  """
  def register_changeset(user, attrs) do
    permitted_attrs = ~w(
      username
      email
      password
      avatar
      is_admin
      bio
      website
      github_handle
      company_id
      location_id
    )a

    register_attrs = ~w(
      username
      email
      password
    )a

    user
    |> cast(attrs, permitted_attrs)
    |> validate_required(register_attrs)
    |> validate_length(:username, min: 3, max: 12)
    |> validate_format(:username, Regexp.username())
    |> unique_constraint(:username)
    |> validate_length(:email, min: 1, max: 20)
    |> validate_format(:email, Regexp.email())
    |> unique_constraint(:email)
    |> put_pass_hash()
  end

  @doc """
  Change user password.
  """
  @spec update_password_changeset(User.t(), map()) :: Ecto.Changeset.t()
  def update_password_changeset(user, attrs) do
    required_attrs = ~w(
      password
      password_confirmation
    )a

    user
    |> cast(attrs, required_attrs)
    |> validate_required(required_attrs)
    |> validate_password_confirmation
    |> put_pass_hash
  end

  # def forgot_password_changeset(user, attrs) do
  #   required_attrs = ~w(email)a

  #   user
  #   |> cast(attrs, required_attrs)
  #   |> validate_required(required_attrs)
  # end

  @doc """
  Reset Password when user forgot password.
  """
  @spec reset_password_changeset(User.t(), map()) :: Ecto.Changeset.t()
  def reset_password_changeset(user, attrs) do
    required_attrs = ~w(
      reset_password_token
      password
      password_confirmation
    )a

    user
    |> cast(attrs, required_attrs)
    |> validate_required(required_attrs)
    |> validate_password_confirmation
    |> put_pass_hash
  end

  defp validate_password_confirmation(%{changes: changes} = changeset) do
    if changes[:password] == changes[:password_confirmation],
      do: changeset,
      else: add_error(changeset, :password_confirmation, "must match password")
  end

  defp put_pass_hash(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true, changes: %{password: password}} ->
        put_change(changeset, :password_hash, Bcrypt.hashpwsalt(password))

      _ ->
        changeset
    end
  end
end


================================================
FILE: lib/mipha/accounts/user_team.ex
================================================
defmodule Mipha.Accounts.UserTeam do
  @moduledoc false

  use Ecto.Schema
  import Ecto.Changeset
  alias Mipha.Accounts.{User, Team}

  schema "users_teams" do
    field :role, :string
    field :status, :string

    belongs_to :team, Team
    belongs_to :user, User

    timestamps()
  end

  @doc false
  def changeset(user_team, attrs) do
    user_team
    |> cast(attrs, [:user_id, :team_id])
    |> validate_required([:user_id, :team_id])
  end
end


================================================
FILE: lib/mipha/application.ex
================================================
defmodule Mipha.Application do
  @moduledoc false

  use Application

  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec

    # Define workers and child supervisors to be supervised
    children = [
      # Start the Ecto repository
      supervisor(Mipha.Repo, []),
      {Phoenix.PubSub, name: Mipha.PubSub},
      # Start the endpoint when the application starts
      supervisor(MiphaWeb.Endpoint, []),
      supervisor(MiphaWeb.Presence, []),
      # Start your own worker by calling: Mipha.Worker.start_link(arg1, arg2, arg3)
      worker(Cachex, [:app_cache, []]),
      worker(PlugAttack.Storage.Ets, [Mipha.PlugAttack.Storage, [clean_period: 60_000]])
      # worker(Mipha.Worker, [arg1, arg2, arg3]),
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Mipha.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  def config_change(changed, _new, removed) do
    MiphaWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end


================================================
FILE: lib/mipha/collections/collection.ex
================================================
defmodule Mipha.Collections.Collection do
  @moduledoc false

  use Ecto.Schema
  import Ecto.{Changeset, Query}

  alias Mipha.{
    Topics.Topic,
    Accounts.User,
    Collections.Collection
  }

  @type t :: %Collection{}

  schema "collections" do
    belongs_to :user, User
    belongs_to :topic, Topic

    timestamps()
  end

  @doc """
  Filters collection by a user.
  """
  @spec by_user(Ecto.Queryable.t(), User.t()) :: Ecto.Query.t()
  def by_user(query \\ __MODULE__, %User{id: user_id}),
    do: where(query, [..., c], c.user_id == ^user_id)

  @doc """
  Filters collection by a topic.
  """
  @spec by_topic(Ecto.Queryable.t(), Topic.t()) :: Ecto.Query.t()
  def by_topic(query \\ __MODULE__, %Topic{id: topic_id}),
    do: where(query, [..., c], c.topic_id == ^topic_id)

  @doc false
  def changeset(collection, attrs) do
    permitted_attrs = ~w(
      user_id
      topic_id
    )a

    required_attrs = ~w(
      user_id
      topic_id
    )a

    collection
    |> cast(attrs, permitted_attrs)
    |> validate_required(required_attrs)
  end
end


================================================
FILE: lib/mipha/collections/collections.ex
================================================
defmodule Mipha.Collections do
  @moduledoc """
  The Collections context.
  """

  import Ecto.Query, warn: false
  alias Mipha.Repo

  alias Mipha.Collections.Collection
  alias Mipha.Accounts.User
  alias Mipha.Topics.Topic

  @doc """
  Returns the list of collections.

  ## Examples

      iex> list_collections()
      [%Collection{}, ...]

  """
  def list_collections do
    Repo.all(Collection)
  end

  @doc """
  Gets a single collection.

  Raises `Ecto.NoResultsError` if the Collection does not exist.

  ## Examples

      iex> get_collection!(123)
      %Collection{}

      iex> get_collection!(456)
      ** (Ecto.NoResultsError)

  """
  def get_collection!(id), do: Repo.get!(Collection, id)

  @doc """
  Creates a collection.

  ## Examples

      iex> create_collection(%{field: value})
      {:ok, %Collection{}}

      iex> create_collection(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_collection(attrs \\ %{}) do
    %Collection{}
    |> Collection.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a collection.

  ## Examples

      iex> update_collection(collection, %{field: new_value})
      {:ok, %Collection{}}

      iex> update_collection(collection, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_collection(%Collection{} = collection, attrs) do
    collection
    |> Collection.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Collection.

  ## Examples

      iex> delete_collection(collection)
      {:ok, %Collection{}}

      iex> delete_collection(collection)
      {:error, %Ecto.Changeset{}}

  """
  def delete_collection(%Collection{} = collection) do
    Repo.delete(collection)
  end

  @spec delete_collection(Keyword.t()) :: {:ok, Collection.t()} | nil
  def delete_collection(clauses) when length(clauses) == 2 do
    clauses
    |> get_collection
    |> Repo.delete()
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking collection changes.

  ## Examples

      iex> change_collection(collection)
      %Ecto.Changeset{source: %Collection{}}

  """
  def change_collection(%Collection{} = collection) do
    Collection.changeset(collection, %{})
  end

  @doc """
  Gets the collection count of a user.

  ## Examples

    iex> get_collection_count(%User{})
    42

  """
  @spec get_collection_count(User.t()) :: non_neg_integer()
  def get_collection_count(%User{} = user) do
    user
    |> Collection.by_user()
    |> Repo.aggregate(:count, :id)
  end

  @doc """
  Gets a star.

  ## Examples

      iex> get_collection(user_id: 123, topic_id: 123)
      %Star{}

      iex> get_collection(user_id: 123, topic_id: 456)
      nil

  """
  @spec get_collection(Keyword.t()) :: Collection.t() | nil
  def get_collection(clauses) when length(clauses) == 2, do: Repo.get_by(Collection, clauses)

  @doc """
  Returns `true` if the user has starred the starrable.
  `false` otherwise.

  ## Examples

      iex> has_collected?(user: %User{}, topic: %Topic{})
      true

      iex> has_collected?(user: %User{}, topic: %Topic{})
      false

  """
  @spec has_collected?(Keyword.t()) :: boolean
  def has_collected?(clauses) do
    %User{id: user_id} = Keyword.get(clauses, :user)
    %Topic{id: topic_id} = Keyword.get(clauses, :topic)

    opts = [user_id: user_id, topic_id: topic_id]
    !!get_collection(opts)
  end

  @doc """
  Insert a collection.

  ## Examples

      iex> insert_collection(attrs)
      {:ok, %Follow{}}

      iex> insert_collection(attrs)
      {:error, %Ecto.Changeset{}}

  """
  def insert_collection(attrs \\ %{}) do
    %Collection{}
    |> Collection.changeset(attrs)
    |> Repo.insert()
  end
end


================================================
FILE: lib/mipha/collections/queries.ex
================================================
defmodule Mipha.Collections.Queries do
  @moduledoc false

  import Ecto.Query
  alias Mipha.Collections.Collection

  @doc """
  Filter the list of collections.

  ## Examples

      iex> cond_collections(params)
      Ecto.Query.t()

  """
  @spec cond_collections(Keyword.t()) :: Ecto.Query.t()
  def cond_collections(opts \\ []) do
    opts
    |> filter_from_clauses
    |> preload([:user, [topic: :node]])
  end

  defp filter_from_clauses(opts) do
    cond do
      Keyword.has_key?(opts, :user) -> opts |> Keyword.get(:user) |> Collection.by_user()
      Keyword.has_key?(opts, :topic) -> opts |> Keyword.get(:topic) |> Collection.by_topic()
      true -> Collection
    end
  end
end


================================================
FILE: lib/mipha/factory.ex
================================================
defmodule Mipha.Factory do
  @moduledoc """
  ExMachina Factory funcs to use in tests.

  ## Examples

      iex> Factory.insert(:topic)
      %Topic{}

  """

  use ExMachina.Ecto, repo: Mipha.Repo

  alias Mipha.Accounts.{User, Company, Team}
  alias Mipha.Topics.{Topic, Node}
  alias Mipha.Replies.Reply
  alias Mipha.Collections.Collection
  alias Mipha.Notifications.{Notification, UserNotification}
  alias Mipha.Stars.Star
  alias Mipha.Follows.Follow

  @spec user_factory :: User.t()
  def user_factory do
    %User{
      username: sequence(:username, &"user#{&1}"),
      email: sequence(:email, &"user#{&1}@elixir-mipha.com"),
      is_admin: false
    }
  end

  @spec topic_factory :: Topic.t()
  def topic_factory do
    %Topic{
      user: build(:user),
      node: build(:node),
      title: sequence(:title, &"topic-title#{&1}"),
      body: sequence(:body, &"topic-body#{&1}")
    }
  end

  @spec node_factory :: Node.t()
  def node_factory do
    %Node{
      name: sequence(:name, &"node-name#{&1}")
    }
  end

  @spec reply_factory :: Reply.t()
  def reply_factory do
    %Reply{
      content: sequence(:content, &"reply-content#{&1}"),
      user: build(:user),
      topic: build(:topic)
    }
  end

  @spec collection_factory :: Collection.t()
  def collection_factory do
    %Collection{
      topic: build(:topic),
      user: build(:user)
    }
  end

  @spec notification_factory :: Notification.t()
  def notification_factory do
    %Notification{
      actor: build(:user),
      action: :followed
    }
  end

  @spec user_notification_factory :: UserNotification.t()
  def user_notification_factory do
    %UserNotification{
      user: build(:user),
      notification: build(:notification)
    }
  end

  @spec star_factory :: Star.t()
  def star_factory do
    %Star{
      user: build(:user)
    }
  end

  @spec follow_factory :: Follow.t()
  def follow_factory do
    %Follow{
      follower: build(:user),
      user: build(:user)
    }
  end

  @spec company_factory :: Company.t()
  def company_factory do
    %Company{
      name: sequence(:name, &"company-name#{&1}")
    }
  end

  @spec team_factory :: Team.t()
  def team_factory do
    %Team{
      name: sequence(:name, &"team-name#{&1}"),
      owner: build(:user)
    }
  end
end


================================================
FILE: lib/mipha/follows/follow.ex
================================================
defmodule Mipha.Follows.Follow do
  @moduledoc false

  use Ecto.Schema
  import Ecto.{Changeset, Query}

  alias Mipha.{
    Repo,
    Accounts.User,
    Follows.Follow
  }

  @type t :: %Follow{}

  schema "follows" do
    belongs_to :user, User
    belongs_to :follower, User, foreign_key: :follower_id

    timestamps()
  end

  @doc """
  Filters the follows by follower.
  """
  @spec by_follower(Ecto.Queryable.t(), User.t()) :: Ecto.Query.t()
  def by_follower(query \\ __MODULE__, %User{id: follower_id}),
    do: from(f in query, where: f.follower_id == ^follower_id)

  @doc """
  Filters the follows by followed user.
  """
  @spec by_user(Ecto.Queryable.t(), User.t()) :: Ecto.Query.t()
  def by_user(query \\ __MODULE__, %User{id: user_id}),
    do: from(f in query, where: f.user_id == ^user_id)

  @doc """
  Preloads the user of a topic.
  """
  @spec preload_user(t()) :: t()
  def preload_user(follow), do: Repo.preload(follow, [:user])

  @doc false
  def changeset(follow, attrs) do
    permitted_attrs = ~w(
      user_id
      follower_id
    )a

    required_attrs = ~w(
      user_id
      follower_id
    )a

    follow
    |> cast(attrs, permitted_attrs)
    |> validate_required(required_attrs)
  end
end


================================================
FILE: lib/mipha/follows/follows.ex
================================================
defmodule Mipha.Follows do
  @moduledoc """
  The Follows context.
  """

  import Ecto.Query, warn: false
  alias Ecto.Multi

  alias Mipha.{
    Repo,
    Follows,
    Accounts,
    Notifications
  }

  alias Follows.Follow
  alias Accounts.User

  @doc """
  Returns the list of follows.

  ## Examples

      iex> list_follows()
      [%Follow{}, ...]

  """
  def list_follows do
    Repo.all(Follow)
  end

  @doc """
  Gets a single follow.

  Raises `Ecto.NoResultsError` if the Follow does not exist.

  ## Examples

      iex> get_follow!(123)
      %Follow{}

      iex> get_follow!(456)
      ** (Ecto.NoResultsError)

  """
  def get_follow!(id), do: Repo.get!(Follow, id)

  @doc """
  Gets a single follow.

  ## Examples

      iex> get_follow(123)
      %Follow{}

      iex> get_follow(456)
      nil

      iex> get_follow(follower_id: 123, user_id: 456)
      %Follow{}

      iex> get_follow(follower_id: 123, user_id: 458)
      nil

  """
  @spec get_follow(String.t() | non_neg_integer) :: Follow.t() | nil
  def get_follow(id) when not is_list(id), do: Repo.get(Follow, id)

  @spec get_follow(Keyword.t()) :: Follow.t() | nil
  def get_follow(clauses) when length(clauses) == 2, do: Repo.get_by(Follow, clauses)

  @doc """
  Creates a follow.

  ## Examples

      iex> create_follow(%{field: value})
      {:ok, %Follow{}}

      iex> create_follow(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_follow(attrs \\ %{}) do
    %Follow{}
    |> Follow.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a follow.

  ## Examples

      iex> update_follow(follow, %{field: new_value})
      {:ok, %Follow{}}

      iex> update_follow(follow, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_follow(%Follow{} = follow, attrs) do
    follow
    |> Follow.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Follow.

  ## Examples

      iex> delete_follow(follow)
      {:ok, %Follow{}}

      iex> delete_follow(follow)
      {:error, %Ecto.Changeset{}}

  """
  @spec delete_follow(Follow.t()) :: {:ok, Follow.t()} | {:error, Ecto.Changeset.t()}
  def delete_follow(%Follow{} = follow) do
    Repo.delete(follow)
  end

  @spec delete_follow(Keyword.t()) :: {:ok, Follow.t()} | {:error, Ecto.Changeset.t()}
  def delete_follow(clauses) when length(clauses) == 2 do
    clauses
    |> get_follow
    |> Repo.delete()
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking follow changes.

  ## Examples

      iex> change_follow(follow)
      %Ecto.Changeset{source: %Follow{}}

  """
  def change_follow(%Follow{} = follow) do
    Follow.changeset(follow, %{})
  end

  @doc """
  Unfollow user.

  ## Examples

      follow_user(follower: %User{}, user: %User{})
      {:ok, follow}

      follow_user(follower: %User{}, user: %User{})
      {:error, _}

  """
  @spec unfollow_user(Keyword.t()) :: {:ok, Follow.t()} | {:error, any()}
  def unfollow_user(follower: follower, user: user) do
    opts = [follower: follower, user: user]

    if can_follow?(opts) do
      if has_followed?(opts) do
        delete_follow(user_id: user.id, follower_id: follower.id)
      else
        {:error, "Unfollow already."}
      end
    else
      {:error, "Can not follower yourself."}
    end
  end

  @doc """
  Follow user.
  ## Examples

      follow_user(follower: %User{}, user: %User{})
      {:ok, follow}

      follow_user(follower: %User{}, user: %User{})
      {:error, _}

  """
  @spec follow_user(Keyword.t()) :: {:ok, Follow.t()} | {:error, any()}
  def follow_user(follower: follower, user: user) do
    opts = [follower: follower, user: user]

    if can_follow?(opts) do
      if has_followed?(opts) do
        {:error, "Follow already."}
      else
        insert_follow(%{user_id: user.id, follower_id: follower.id})
      end
    else
      {:error, "Can't follower yourself."}
    end
  end

  @doc """
  Returns `true` if the follower can follow the followable.
  `false` otherwise.
  """
  @spec can_follow?(Keyword.t()) :: boolean
  def can_follow?(clauses) do
    %User{id: follower_id} = Keyword.get(clauses, :follower)
    %User{id: user_id} = Keyword.get(clauses, :user)

    (user_id == follower_id && false) || true
  end

  @doc """
  Returns `true` if the user has followed the followable.
  `false` otherwise.

  ## Examples

      iex> has_followed?(follower: %User{}, user: %User{})
      true

      iex> has_followed?(follower: %User{}, topic: %Topic{})
      false

  """
  @spec has_followed?(Keyword.t()) :: boolean
  def has_followed?(clauses) do
    # FIXME 这里添加 can_follow? 的逻辑
    %User{id: follower_id} = Keyword.get(clauses, :follower)
    %User{id: user_id} = Keyword.get(clauses, :user)

    opts = [follower_id: follower_id, user_id: user_id]
    !!get_follow(opts)
  end

  @doc """
  Insert a follow.

  ## Examples

      iex> insert_follow(attrs)
      {:ok, %Follow{}}

      iex> insert_follow(attrs)
      {:error, %Ecto.Changeset{}}

  """
  def insert_follow(attrs \\ %{}) do
    follow_changeset = Follow.changeset(%Follow{}, attrs)

    Multi.new()
    |> Multi.insert(:follow, follow_changeset)
    |> notify_followee_of_follow()
    |> Repo.transaction()
  end

  defp notify_followee_of_follow(multi) do
    insert_notification_fn = fn _repo, %{follow: follow} ->
      followee =
        follow
        |> Follow.preload_user()
        |> Map.fetch!(:user)

      notified_users = [followee]

      notification_attrs = %{
        user_id: followee.id,
        actor_id: follow.follower_id,
        action: "followed",
        notified_users: notified_users
      }

      case Notifications.insert_notification(notification_attrs) do
        {:ok, %{notification: notification}} -> {:ok, notification}
        {:error, _, reason, _} -> {:error, reason}
      end
    end

    Multi.run(multi, :notification_of_follow, insert_notification_fn)
  end

  @doc """
  Gets the follower count of a followable.

  ## Examples

      iex> get_follower_count([user: %User{}])
      4

      iex> get_follower_count([artwork: %Artwork{}])
      8

      iex> get_follower_count([topic: %Topic{}])
      12

  """
  @spec get_follower_count(Keyword.t()) :: non_neg_integer()
  def get_follower_count(clauses) do
    query =
      clauses
      |> Keyword.get(:user)
      |> Follow.by_user()

    query
    |> join(:inner, [c], u in assoc(c, :follower))
    |> Repo.aggregate(:count, :id)
  end

  @doc """
  Gets the followee count of a user.

  ## Examples

    iex> get_followee_count(%User{})
    42

  """
  @spec get_followee_count(User.t()) :: non_neg_integer()
  def get_followee_count(%User{} = user) do
    user
    |> Follow.by_follower()
    |> Repo.aggregate(:count, :id)
  end
end


================================================
FILE: lib/mipha/follows/queries.ex
================================================
defmodule Mipha.Follows.Queries do
  @moduledoc """
  Query Follows Contetxt.
  """
  import Ecto.Query
  alias Mipha.Follows.Follow

  @doc """
  Filter the list of topics.

  ## Examples

      iex> cond_follows(params)
      Ecto.Query.t()

      iex> cond_follows(params, user: %User{})
      Ecto.Query.t()

      iex> cond_follows(params, follower: %User{})
      Ecto.Query.t()

  """
  @spec cond_follows(Keyword.t()) :: Ecto.Query.t()
  def cond_follows(opts \\ []) do
    opts
    |> filter_from_clauses
    |> preload([:user, :follower])
  end

  defp filter_from_clauses(opts) do
    cond do
      Keyword.has_key?(opts, :follower) -> opts |> Keyword.get(:follower) |> Follow.by_follower()
      Keyword.has_key?(opts, :user) -> opts |> Keyword.get(:user) |> Follow.by_user()
      true -> Follow
    end
  end
end


================================================
FILE: lib/mipha/mailer.ex
================================================
defmodule Mipha.Mailer do
  @moduledoc false

  use Bamboo.Mailer, otp_app: :mipha
end


================================================
FILE: lib/mipha/markdown/auto_linker.ex
================================================
defmodule Mipha.Markdown.AutoLinker do
  @moduledoc """
  AutoLinker will run on a string and return the string with any naked links
  wrapped in an `a` tag with the link as both content and href..
  """

  # A RegEx to match any URL that has spaces or newlines on either side of it.
  @url_regex ~r{([ \n]+|^)(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?}

  def run(body) do
    @url_regex
    |> Regex.replace(body, "\\1<a href=\"\\2\://\\3\\4\\5\">\\2://\\3\\4\\5</a>")
  end
end


================================================
FILE: lib/mipha/markdown/embed_video_replacer.ex
================================================
defmodule Mipha.Markdown.EmbedVideoReplacer do
  @moduledoc false

  @youtube_url_regex ~r{(\s|^|<div>|<br>)(https?://)(www.)?(youtube\.com/watch\?v=|youtu\.be/|youtube\.com/watch\?feature=player_embedded&v=)([A-Za-z0-9_\-]*)(\&\S+)?(\?\S+)?}
  @youku_url_regex ~r{(\s|^|<div>|<br>)(http?://)(v\.youku\.com/v_show/id_)([a-zA-Z0-9\-_\=]*)(\.html)(\&\S+)?(\?\S+)?}
  @youku_base_url "//player.youku.com/embed/"
  @youtube_base_url "//www.youtube.com/embed/"

  def run(body) do
    cond do
      Regex.match?(@youtube_url_regex, body) == true ->
        @youtube_url_regex
        |> Regex.replace(body, &youtube_replacer/1)

      Regex.match?(@youku_url_regex, body) == true ->
        @youku_url_regex
        |> Regex.replace(body, &youku_replacer/1)

      true ->
        body
    end
  end

  defp youku_replacer(whole_match) do
    youku_id =
      @youku_url_regex
      |> Regex.run(whole_match)
      |> Enum.at(4)

    embed_tag("#{@youku_base_url}#{youku_id}")
  end

  defp youtube_replacer(whole_match) do
    youtube_id =
      @youtube_url_regex
      |> Regex.run(whole_match)
      |> Enum.at(5)

    embed_tag("#{@youtube_base_url}#{youtube_id}")
  end

  defp embed_tag(src) do
    ~s(<span class="embed-responsive embed-responsive-16by9">
    <iframe class="embed-responsive-item" src="#{src}" allowfullscreen></iframe>
    </span>)
  end
end


================================================
FILE: lib/mipha/markdown/emoji_replacer.ex
================================================
defmodule Mipha.Markdown.EmojiReplacer do
  @moduledoc """
  EmojiReplacer will run on a string and return the string with any emoji marks
  (i.e. :poop:) replaced with their emoji counterpart.
  """

  # A RegEx to match any emoji mark
  @emoji_regex ~r{:([a-z]+):}

  def run(body) do
    @emoji_regex
    |> Regex.replace(body, &emojify_short_name/2)
  end

  defp emojify_short_name(whole_match, short_name) do
    case Exmoji.from_short_name(short_name) do
      nil -> whole_match
      emoji -> Exmoji.EmojiChar.render(emoji)
    end
  end
end


================================================
FILE: lib/mipha/markdown/html_renderer.ex
================================================
# This is a clone of
# https://raw.githubusercontent.com/pragdave/earmark/ea6382092c931ab4dd6d0dac6425430c78a61a6d/lib/earmark/html_renderer.ex
# Justification for forking this module:
# - Earmark explicitly provides support for this, but we can't reuse the
#   existing render_* functions as they are private.
# - No existing ability to customize the classes that are produced for the `pre`
#   tag, which is necessary to use http://prismjs.com/plugins/line-numbers/

defmodule Mipha.Markdown.HtmlRenderer do
  @moduledoc false

  alias Earmark.Block
  alias Earmark.Context
  alias Earmark.Options
  import Earmark.Inline, only: [convert: 3]
  import Earmark.Helpers, only: [escape: 2]
  import Earmark.Helpers.HtmlHelpers
  import Earmark.Message, only: [add_messages_from: 2, add_messages: 2, get_messages: 1]
  import Earmark.Context, only: [append: 2, set_value: 2]

  alias Mipha.Markdown.{EmojiReplacer, MentionReplacer, AutoLinker, EmbedVideoReplacer}

  def render(blocks, %Context{options: %Options{mapper: mapper}} = context) do
    messages = get_messages(context)

    {contexts, html} =
      blocks
      |> mapper.(&render_block(&1, put_in(context.options.messages, [])))
      |> Enum.unzip()

    all_messages =
      contexts
      |> Enum.reduce(messages, fn ctx, messages1 -> messages1 ++ get_messages(ctx) end)

    {put_in(context.options.messages, all_messages), html |> IO.iodata_to_binary()}
  end

  # Paragraph
  defp render_block(%Block.Para{lnb: lnb, lines: lines, attrs: attrs}, context) do
    lines =
      lines
      |> convert(lnb, context)
      |> transform_value(&EmojiReplacer.run/1)
      |> transform_value(&EmbedVideoReplacer.run/1)
      |> transform_value(&MentionReplacer.run/1)
      |> transform_value(&AutoLinker.run/1)

    add_attrs!(lines, "<p>#{lines.value}</p>\n", attrs, [], lnb)
  end

  # Html
  defp render_block(%Block.Html{html: html}, context) do
    {context, Enum.intersperse(html, ?\n)}
  end

  defp render_block(%Block.HtmlOther{html: html}, context) do
    {context, Enum.intersperse(html, ?\n)}
  end

  # Ruler
  defp render_block(%Block.Ruler{lnb: lnb, type: "-", attrs: attrs}, context) do
    add_attrs!(context, "<hr/>\n", attrs, [{"class", ["thin"]}], lnb)
  end

  defp render_block(%Block.Ruler{lnb: lnb, type: "_", attrs: attrs}, context) do
    add_attrs!(context, "<hr/>\n", attrs, [{"class", ["medium"]}], lnb)
  end

  defp render_block(%Block.Ruler{lnb: lnb, type: "*", attrs: attrs}, context) do
    add_attrs!(context, "<hr/>\n", attrs, [{"class", ["thick"]}], lnb)
  end

  # Heading
  defp render_block(
         %Block.Heading{lnb: lnb, level: level, content: content, attrs: attrs},
         context
       ) do
    converted = convert(content, lnb, context)
    html = "<h#{level}>#{converted.value}</h#{level}>\n"
    add_attrs!(converted, html, attrs, [], lnb)
  end

  # Blockquote
  defp render_block(%Block.BlockQuote{lnb: lnb, blocks: blocks, attrs: attrs}, context) do
    {context1, body} = render(blocks, context)
    html = "<blockquote>#{body}</blockquote>\n"
    add_attrs!(context1, html, attrs, [], lnb)
  end

  # Table
  defp render_block(
         %Block.Table{lnb: lnb, header: header, rows: rows, alignments: aligns, attrs: attrs},
         context
       ) do
    cols = for _align <- aligns, do: "<col>\n"
    {context1, html} = add_attrs!(context, "<table>\n", attrs, [], lnb)
    html = [html, "<colgroup>\n", cols, "</colgroup>\n"]
    context2 = set_value(context1, html)

    context3 =
      if header do
        append(add_trs(append(context2, "<thead>\n"), [header], "th", aligns, lnb), "</thead>\n")
      else
        # Maybe an error, needed append(context, html)
        context2
      end

    context4 = add_trs(context3, rows, "td", aligns, lnb)

    {context4, [context4.value, "</table>\n"]}
  end

  # Code
  defp render_block(
         %Block.Code{lnb: lnb, language: language, attrs: attrs} = block,
         %Context{options: options} = context
       ) do
    class =
      if language, do: ~s{ class="#{code_classes(language, options.code_class_prefix)}"}, else: ""

    tag = ~s[<pre><code#{class}>]
    lines = options.render_code.(block)
    html = ~s[#{tag}#{lines}</code></pre>\n]
    add_attrs!(context, html, attrs, [], lnb)
  end

  # Lists
  defp render_block(
         %Block.List{lnb: lnb, type: type, blocks: items, attrs: attrs, start: start},
         context
       ) do
    {context1, content} = render(items, context)
    html = "<#{type}#{start}>\n#{content}</#{type}>\n"
    add_attrs!(context1, html, attrs, [], lnb)
  end

  # format a single paragraph list item, and remove the para tags
  defp render_block(
         %Block.ListItem{lnb: lnb, blocks: blocks, spaced: false, attrs: attrs},
         context
       )
       when length(blocks) == 1 do
    {context1, content} = render(blocks, context)
    content = Regex.replace(~r{</?p>}, content, "")
    html = "<li>#{content}</li>\n"
    add_attrs!(context1, html, attrs, [], lnb)
  end

  # format a spaced list item
  defp render_block(%Block.ListItem{lnb: lnb, blocks: blocks, attrs: attrs}, context) do
    {context1, content} = render(blocks, context)
    html = "<li>#{content}</li>\n"
    add_attrs!(context1, html, attrs, [], lnb)
  end

  # Footnote Block
  defp render_block(%Block.FnList{blocks: footnotes}, context) do
    items =
      Enum.map(footnotes, fn note ->
        blocks = append_footnote_link(note)
        %Block.ListItem{attrs: "#fn:#{note.number}", type: :ol, blocks: blocks}
      end)

    {context1, html} = render_block(%Block.List{type: :ol, blocks: items}, context)
    {context1, Enum.join([~s[<div class="footnotes">], "<hr>", html, "</div>"], "\n")}
  end

  defp render_block(%Block.Ial{verbatim: verbatim}, context) do
    {context, "<p>{:#{verbatim}}</p>\n"}
  end

  # IDDef is ignored #
  defp render_block(%Block.IdDef{}, context), do: {context, ""}

  # Plugins
  defp render_block(%Block.Plugin{lines: lines, handler: handler}, context) do
    case handler.as_html(lines) do
      html when is_list(html) -> {context, html}
      {html, errors} -> {add_messages(context, errors), html}
      html -> {context, [html]}
    end
  end

  # And here are the inline renderers #
  def br, do: "<br/>"
  def codespan(text), do: ~s[<code class="inline">#{text}</code>]
  def em(text), do: "<em>#{text}</em>"
  def strong(text), do: "<strong>#{text}</strong>"
  def strikethrough(text), do: "<del>#{text}</del>"

  def link(url, text), do: ~s[<a href="#{url}">#{text}</a>]
  def link(url, text, nil), do: ~s[<a href="#{url}">#{text}</a>]
  def link(url, text, title), do: ~s[<a href="#{url}" title="#{title}">#{text}</a>]

  def image(path, alt, nil) do
    ~s[<img src="#{path}" alt="#{alt}"/>]
  end

  def image(path, alt, title) do
    ~s[<img src="#{path}" alt="#{alt}" title="#{title}"/>]
  end

  def footnote_link(ref, backref, number),
    do: ~s[<a href="##{ref}" id="#{backref}" class="footnote" title="see footnote">#{number}</a>]

  # Table rows
  def add_trs(context, rows, tag, aligns, lnb) do
    numbered_rows =
      rows
      |> Enum.zip(Stream.iterate(lnb, &(&1 + 1)))

    # for {row, lnb1} <- numbered_rows, do: "<tr>\n#{add_tds(context, row, tag, aligns, lnb1)}\n</tr>\n"
    numbered_rows
    |> Enum.reduce(context, fn {row, lnb}, ctx ->
      append(add_tds(append(ctx, "<tr>\n"), row, tag, aligns, lnb), "\n</tr>\n")
    end)
  end

  defp add_tds(context, row, tag, aligns, lnb) do
    Enum.reduce(1..length(row), context, add_td_fn(row, tag, aligns, lnb))
  end

  defp add_td_fn(row, tag, aligns, lnb) do
    fn n, ctx ->
      style =
        case Enum.at(aligns, n - 1, :default) do
          :default -> ""
          align -> " style=\"text-align: #{align}\""
        end

      col = Enum.at(row, n - 1)
      converted = convert(col, lnb, ctx)
      append(add_messages_from(ctx, converted), "<#{tag}#{style}>#{converted.value}</#{tag}>")
    end
  end

  # Append Footnote Return Link
  def append_footnote_link(%Block.FnDef{} = note) do
    fnlink =
      ~s[<a href="#fnref:#{note.number}" title="return to article" class="reversefootnote">&#x21A9;</a>]

    [last_block | blocks] = Enum.reverse(note.blocks)
    last_block = append_footnote_link(last_block, fnlink)

    [last_block | blocks]
    |> Enum.reverse()
    |> List.flatten()
  end

  def append_footnote_link(%Block.Para{lines: lines} = block, fnlink) do
    [last_line | lines] = Enum.reverse(lines)
    last_line = "#{last_line}&nbsp;#{fnlink}"
    [put_in(block.lines, Enum.reverse([last_line | lines]))]
  end

  def append_footnote_link(block, fnlink) do
    [block, %Block.Para{lines: fnlink}]
  end

  def render_code(%Block.Code{lines: lines}) do
    lines |> Enum.join("\n") |> escape(true)
  end

  defp code_classes(language, prefix) do
    ["" | String.split(prefix || "")]
    |> Enum.map(fn pfx -> "#{pfx}#{language}" end)
    |> Enum.join(" ")
  end

  defp transform_value(context, transformer) do
    %{context | value: transformer.(context.value)}
  end
end


================================================
FILE: lib/mipha/markdown/markdown_guides.md
================================================
# Guide

这是一篇讲解如何正确使用 **Markdown** 的排版示例,学会这个很有必要,能让你的文章有更佳清晰的排版。

> 引用文本:Markdown is a text formatting syntax inspired

## 语法指导

### 普通内容

这段内容展示了在内容里面一些小的格式,比如:

- **加粗** - `**加粗**`
- *倾斜* - `*倾斜*`
- ~~删除线~~ - `~~删除线~~`
- `Code 标记` - `\`Code 标记\``
- [超级链接](http://github.com) - `[超级链接](http://github.com)`
- [username@gmail.com](mailto:username@gmail.com) - `[username@gmail.com](mailto:username@gmail.com)`

### 提及用户

@foo @bar @someone @zven ... 通过 @ 可以在发帖和回帖里面提及用户,信息提交以后,被提及的用户将会收到系统通知。以便让他来关注这个帖子或回帖。

### 表情符号 Emoji

支持表情符号,你可以用系统默认的 Emoji 符号(无法支持 Windows 用户)。
也可以用图片的表情,输入 `:` 将会出现智能提示。

#### 一些表情例子

:smile: :laughing: :dizzy_face: :sob: :cold_sweat: :sweat_smile:  :cry: :triumph: :heart_eyes: :relaxed: :sunglasses: :weary:

:+1: :-1: :100: :clap: :bell: :gift: :question: :bomb: :heart: :coffee: :cyclone: :bow: :kiss: :pray: :sweat_drops: :hankey: :exclamation: :anger:

### 大标题 - Heading 3

你可以选择使用 H2 至 H6,使用 ##(N) 打头,H1 不能使用,会自动转换成 H2。

> NOTE: 别忘了 # 后面需要有空格!

#### Heading 4

##### Heading 5

###### Heading 6

### 图片

```
![alt 文本](http://image-path.png)
![alt 文本](http://image-path.png "图片 Title 值")
![设置图片宽度高度](http://image-path.png =300x200)
![设置图片宽度](http://image-path.png =300x)
![设置图片高度](http://image-path.png =x200)
```

### 代码块

#### 普通

```
*emphasize*    **strong**
_emphasize_    __strong__
@a = 1
```

#### 语法高亮支持

如果在 \`\`\` 后面更随语言名称,可以有语法高亮的效果哦,比如:

##### 演示 Ruby 代码高亮

```ruby
class PostController < ApplicationController
  def index
    @posts = Post.last_actived.limit(10)
  end
end
```

##### 演示 Rails View 高亮

```erb
<%= @posts.each do |post| %>
<div class="post"></div>
<% end %>
```

##### 演示 YAML 文件

```yml
zh-CN:
  name: 姓名
  age: 年龄
```

> Tip: 语言名称支持下面这些: `ruby`, `python`, `js`, `html`, `erb`, `css`, `coffee`, `bash`, `json`, `yml`, `xml` ...

### 有序、无序列表

#### 无序列表

- Ruby
  - Rails
    - ActiveRecord
- Go
  - Gofmt
  - Revel
- Node.js
  - Koa
  - Express

#### 有序列表

1. Node.js
  1. Express
  2. Koa
  3. Sails
2. Ruby
  1. Rails
  2. Sinatra
3. Go

### 表格

如果需要展示数据什么的,可以选择使用表格哦

| header 1 | header 3 |
| -------- | -------- |
| cell 1   | cell 2   |
| cell 3   | cell 4   |
| cell 5   | cell 6   |

### 段落

留空白的换行,将会被自动转换成一个段落,会有一定的段落间距,便于阅读。

请注意后面 Markdown 源代码的换行留空情况。

### 视频插入

目前支持 Youtube 和 Youku 两家的视频插入,你只需要复制视频播放页面,浏览器地址栏的网页 URL 地址,并粘贴到话题/回复文本框,提交以后将自动转换成视频播放器。

#### 例如

**Youtube**

https://www.youtube.com/watch?v=CvVvwh3BRq8

**Vimeo**

https://vimeo.com/199770305

**Youku**

http://v.youku.com/v_show/id_XMjQzMTY1MDk3Ng==.html

···

================================================
FILE: lib/mipha/markdown/mention_replacer.ex
================================================
defmodule Mipha.Markdown.MentionReplacer do
  @moduledoc """
  Replacer
  """

  alias Mipha.Accounts

  @user_regex ~r{@([a-z1-9]+)}

  def run(body) do
    @user_regex
    |> Regex.replace(body, &existed_user_replacer/2)
  end

  defp existed_user_replacer(whole_match, match_name) do
    case Accounts.get_user_by_username(match_name) do
      nil -> whole_match
      user -> ~s(<a href="/u/#{user.username}" title="#{user.username}">#{user.username}</a>)
    end
  end
end


================================================
FILE: lib/mipha/markdown.ex
================================================
defmodule Mipha.Markdown do
  @moduledoc """
  Sanitize a string's HTML, then render the string as markdown in the Mipha style.
  """

  def render(body) do
    body
    |> as_html!()
  end

  defp as_html!(body) do
    body
    |> HtmlSanitizeEx.markdown_html()
    |> Earmark.as_html!(earmark_options())
  end

  def example do
    File.read!("./lib/mipha/markdown/markdown_guides.md")
  end

  def earmark_options do
    %Earmark.Options{
      # Prefix the `code` tag language class, as in `language-elixir`, for
      # proper support from http://prismjs.com/
      code_class_prefix: "language-",
      renderer: Mipha.Markdown.HtmlRenderer,
      gfm: true,
      breaks: true,
      smartypants: false
    }
  end
end


================================================
FILE: lib/mipha/notifications/notification.ex
================================================
defmodule Mipha.Notifications.Notification do
  @moduledoc false

  use Ecto.Schema
  import Ecto.{Changeset, Query}
  import EctoEnum, only: [defenum: 3]

  alias Mipha.Repo
  alias Mipha.Accounts.User
  alias Mipha.Topics.Topic
  alias Mipha.Replies.Reply
  alias Mipha.Notifications.{Notification, UserNotification}

  @type t :: %Notification{}

  # Support as follows
  # :topic_reply_added
  # :topic_starred
  # :reply_starred
  # :reply_comment_added
  # :topic_mentioned
  # :reply_mentioned
  # :followed
  # Follow user action
  #   :topic_added
  #   :topic_reply_added
  defenum(NotificationAction, :notification_action, [
    :topic_reply_added,
    :topic_starred,
    :reply_starred,
    :reply_comment_added,
    :topic_mentioned,
    :reply_mentioned,
    :followed,
    :topic_added
  ])

  schema "notifications" do
    field :action, NotificationAction

    belongs_to :actor, User, foreign_key: :actor_id
    belongs_to :reply, Reply
    belongs_to :topic, Topic
    belongs_to :user, User

    has_many :user_notifications, UserNotification

    many_to_many :notified_users, User, join_through: UserNotification

    timestamps()
  end

  @doc """
  Filters the notifications by actor.
  """
  @spec by_actor(Ecto.Queryable.t(), User.t()) :: Ecto.Query.t()
  def by_actor(query \\ Notification, %User{id: actor_id}),
    do: where(query, [..., n], n.actor_id == ^actor_id)

  @doc """
  Preloads the actor of a notification.
  """
  @spec preload_actor(t()) :: t()
  def preload_actor(%Notification{} = notification), do: Repo.preload(notification, :actor)

  @doc """
  Preloads the topic of a notification.
  """
  @spec preload_topic(t()) :: t()
  def preload_topic(%Notification{} = notification), do: Repo.preload(notification, :topic)

  @doc """
  Preloads the reply of a notification.
  """
  @spec preload_reply(t()) :: t()
  def preload_reply(%Notification{} = notification), do: Repo.preload(notification, :reply)

  @doc """
  Preloads the user of a notification.
  """
  @spec preload_user(t()) :: t()
  def preload_user(%Notification{} = notification), do: Repo.preload(notification, :user)

  @doc false
  def changeset(notification, attrs) do
    permitted_attrs = ~w(
      action
      actor_id
      topic_id
      reply_id
      user_id
    )a

    required_attrs = ~w(
      action
    )a

    notification
    |> cast(attrs, permitted_attrs)
    |> validate_required(required_attrs)
    |> assoc_constraint(:actor)
    |> assoc_constraint(:topic)
    |> assoc_constraint(:reply)
    |> assoc_constraint(:user)
    |> maybe_put_notified_users(attrs)
  end

  @spec maybe_put_notified_users(Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
  defp maybe_put_notified_users(%Ecto.Changeset{} = changeset, attrs) do
    case Map.get(attrs, :notified_users) do
      value when not is_nil(value) -> put_assoc(changeset, :notified_users, value)
      nil -> changeset
    end
  end
end


================================================
FILE: lib/mipha/notifications/notifications.ex
================================================
defmodule Mipha.Notifications do
  @moduledoc """
  The Notifications context.
  """

  import Ecto.Query, warn: false
  alias Ecto.Multi

  alias Mipha.{
    Repo,
    Topics,
    Replies,
    Accounts,
    Notifications
  }

  alias Notifications.{Notification, UserNotification}
  alias Topics.Topic
  alias Replies.Reply
  alias Accounts.User

  @type notification_object :: Topic.t() | Reply.t() | User.t()

  @doc """
  Returns the list of notifications.

  ## Examples

      iex> list_notifications()
      [%Notification{}, ...]

  """
  def list_notifications do
    Repo.all(Notification)
  end

  @doc """
  Gets a single notification.

  Raises `Ecto.NoResultsError` if the Notification does not exist.

  ## Examples

      iex> get_notification!(123)
      %Notification{}

      iex> get_notification!(456)
      ** (Ecto.NoResultsError)

  """
  def get_notification!(id), do: Repo.get!(Notification, id)

  @doc """
  Creates a notification.

  ## Examples

      iex> create_notification(%{field: value})
      {:ok, %Notification{}}

      iex> create_notification(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_notification(attrs \\ %{}) do
    %Notification{}
    |> Notification.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a notification.

  ## Examples

      iex> update_notification(notification, %{field: new_value})
      {:ok, %Notification{}}

      iex> update_notification(notification, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_notification(%Notification{} = notification, attrs) do
    notification
    |> Notification.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Notification.

  ## Examples

      iex> delete_notification(notification)
      {:ok, %Notification{}}

      iex> delete_notification(notification)
      {:error, %Ecto.Changeset{}}

  """
  def delete_notification(%Notification{} = notification) do
    Repo.delete(notification)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking notification changes.

  ## Examples

      iex> change_notification(notification)
      %Ecto.Changeset{source: %Notification{}}

  """
  def change_notification(%Notification{} = notification) do
    Notification.changeset(notification, %{})
  end

  @doc """
  标记单个已读
  """
  @spec read_notification(UserNotification.t()) ::
          {:ok, UserNotification.t()} | {:error, %Ecto.Changeset{}}
  def read_notification(%UserNotification{} = user_notification) do
    attrs = %{read_at: Timex.now()}

    user_notification
    |> UserNotification.update_changeset(attrs)
    |> Repo.update()
  end

  @doc """
  标注所有未读信息为已读
  """
  @spec mark_read_notification(User.t()) :: any
  def mark_read_notification(user) do
    user
    |> UserNotification.by_user()
    |> UserNotification.unread()
    |> Repo.update_all(set: [read_at: Timex.now()])
  end

  @doc """
  清空通知
  """
  @spec clean_notification(User.t()) :: any
  def clean_notification(user) do
    user
    |> UserNotification.by_user()
    |> Repo.delete_all()
  end

  @doc """
  获取通知的发送者。

  ## Examples

      iex> actor(%Notification{})
      %User{}

      iex> actor(%Notification{})
      nil

  """
  @spec actor(Notification.t()) :: User.t() | nil
  def actor(%Notification{} = notification) do
    notification
    |> Notification.preload_actor()
    |> Map.get(:actor)
  end

  @doc """
  获取通知对象 topic || reply || user

  ## Examples

      iex> object(%Notification{})
      %Topic{}

      iex> object(%Notification{})
      %Reply{}

      iex> object(%Notification{})
      %User{}

  """
  @spec object(Notification.t()) :: notification_object
  def object(%Notification{topic_id: topic_id} = notification) when not is_nil(topic_id) do
    notification
    |> Notification.preload_topic()
    |> Map.get(:topic)
  end

  def object(%Notification{reply_id: reply_id} = notification) when not is_nil(reply_id) do
    notification
    |> Notification.preload_reply()
    |> Map.get(:reply)
    |> Reply.preload_topic()
    |> Reply.preload_parent()
  end

  def object(%Notification{user_id: user_id} = notification) when not is_nil(user_id) do
    notification
    |> Notification.preload_user()
    |> Map.get(:user)
  end

  def object(_), do: nil

  @doc """
  获取未读的 Notification 个数
  """
  @spec unread_notification_count(User.t()) :: non_neg_integer()
  def unread_notification_count(user) do
    user
    |> UserNotification.by_user()
    |> UserNotification.unread()
    |> Repo.aggregate(:count, :id)
  end

  @doc """
  Returns the list of users_notifications.

  ## Examples

      iex> list_users_notifications()
      [%UserNotification{}, ...]

  """
  def list_users_notifications do
    Repo.all(UserNotification)
  end

  @doc """
  Gets a single user_notification.

  Raises `Ecto.NoResultsError` if the User notification does not exist.

  ## Examples

      iex> get_user_notification!(123)
      %UserNotification{}

      iex> get_user_notification!(456)
      ** (Ecto.NoResultsError)

  """
  def get_user_notification!(id), do: Repo.get!(UserNotification, id)

  @doc """
  Creates a user_notification.

  ## Examples

      iex> create_user_notification(%{field: value})
      {:ok, %UserNotification{}}

      iex> create_user_notification(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_user_notification(attrs \\ %{}) do
    %UserNotification{}
    |> UserNotification.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Inserts a notification.
  """
  @spec insert_notification(any) :: {:ok, any} | {:error, any, any, any}
  def insert_notification(attrs) do
    Multi.new()
    |> insert_notification(attrs)
    |> Repo.transaction()
  end

  @spec insert_notification(Multi.t(), map()) :: Multi.t()
  defp insert_notification(multi, attrs) do
    notification_changeset = Notification.changeset(%Notification{}, attrs)

    Multi.insert(multi, :notification, notification_changeset)
  end

  @doc """
  Updates a user_notification.

  ## Examples

      iex> update_user_notification(user_notification, %{field: new_value})
      {:ok, %UserNotification{}}

      iex> update_user_notification(user_notification, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_user_notification(%UserNotification{} = user_notification, attrs) do
    user_notification
    |> UserNotification.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a UserNotification.

  ## Examples

      iex> delete_user_notification(user_notification)
      {:ok, %UserNotification{}}

      iex> delete_user_notification(user_notification)
      {:error, %Ecto.Changeset{}}

  """
  def delete_user_notification(%UserNotification{} = user_notification) do
    Repo.delete(user_notification)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking user_notification changes.

  ## Examples

      iex> change_user_notification(user_notification)
      %Ecto.Changeset{source: %UserNotification{}}

  """
  def change_user_notification(%UserNotification{} = user_notification) do
    UserNotification.changeset(user_notification, %{})
  end
end


================================================
FILE: lib/mipha/notifications/queries.ex
================================================
defmodule Mipha.Notifications.Queries do
  @moduledoc false

  import Ecto.Query

  alias Mipha.Accounts.User
  alias Mipha.Notifications.UserNotification

  @doc """
  Returns current_user notification.
  """
  @spec cond_user_notifications(User.t()) :: Ecto.Query.t()
  def cond_user_notifications(%User{} = user) do
    user
    |> UserNotification.by_user()
    |> preload([:user, :notification])
  end
end


================================================
FILE: lib/mipha/notifications/user_notification.ex
================================================
defmodule Mipha.Notifications.UserNotification do
  @moduledoc false

  use Ecto.Schema
  import Ecto.{Changeset, Query}

  alias Mipha.Repo
  alias Mipha.Accounts.User
  alias Mipha.Notifications.{Notification, UserNotification}

  @type t :: %UserNotification{}

  schema "users_notifications" do
    field :read_at, :naive_datetime

    belongs_to :user, User
    belongs_to :notification, Notification

    timestamps()
  end

  @doc """
  Filters the user notifications by user.
  """
  @spec by_user(Ecto.Queryable.t(), User.t()) :: Ecto.Query.t()
  def by_user(query \\ UserNotification, %User{id: user_id}),
    do: where(query, [..., un], un.user_id == ^user_id)

  @doc """
  Gets unread user notifications
  """
  @spec unread(Ecto.Queryable.t()) :: Ecto.Query.t()
  def unread(query \\ __MODULE__),
    do: where(query, [..., un], is_nil(un.read_at))

  @doc """
  Preloads the user of a user notification.
  """
  @spec preload_user(t()) :: t()
  def preload_user(%UserNotification{} = user_notification),
    do: Repo.preload(user_notification, :user)

  @doc """
  标记已读
  """
  @spec update_changeset(t(), map()) :: Ecto.Changeset.t()
  def update_changeset(%UserNotification{} = user_notification, attrs) do
    permitted_attrs = ~w(
      read_at
    )a

    user_notification
    |> cast(attrs, permitted_attrs)
  end

  @doc """
  Changeset
  """
  def changeset(user_notification, attrs) do
    permitted_attrs = ~w(
      user_id
      notification_id
      read_at
    )a

    required_attrs = ~w(
      user_id
      notification_id
    )a

    user_notification
    |> cast(attrs, permitted_attrs)
    |> validate_required(required_attrs)
  end
end


================================================
FILE: lib/mipha/qiniu.ex
================================================
defmodule Mipha.Qiniu do
  @moduledoc false

  @base_url "http://pbfwruvmm.bkt.clouddn.com/"
  @bucket "mipha"

  def upload(path) do
    @bucket
    |> Qiniu.PutPolicy.build()
    |> Qiniu.Uploader.upload(path, key: generate_key())
  end

  @doc """
  获取七牛的链接地址
  """
  def q_url(value) when is_nil(value) do
    @base_url <> "default.jpg"
  end

  def q_url(value) do
    @base_url <> value
  end

  defp generate_key do
    "#{Timex.to_unix(Timex.now())}/#{Enum.random(1..1_000)}.jpg"
  end
end


================================================
FILE: lib/mipha/regexp.ex
================================================
defmodule Mipha.Regexp do
  @moduledoc """
  Regex
  """

  def username do
    ~r/^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){1,64}$/
  end

  def email do
    ~r/^[A-Za-z0-9._%+-+']+@[A-Za-z0-9.-]+\.[A-Za-z]{2,32}$/
  end
end


================================================
FILE: lib/mipha/replies/queries.ex
================================================
defmodule Mipha.Replies.Queries do
  @moduledoc false

  import Ecto.Query
  alias Mipha.Replies.Reply

  @doc """
  Returns the list replies.
  """
  @spec list_replies :: Ecto.Query.t()
  def list_replies, do: Reply

  @doc """
  Filter the list of repies.

  ## Examples

      iex> cond_replies(params)
      Ecto.Query.t()

      iex> cond_replies(params, user: %User{})
      Ecto.Query.t()

  """
  @spec cond_replies(Keyword.t()) :: Ecto.Query.t()
  def cond_replies(opts) do
    opts
    |> filter_from_clauses
    |> preload([:topic, :user])
  end

  defp filter_from_clauses(opts) do
    cond do
      Keyword.has_key?(opts, :user) -> opts |> Keyword.get(:user) |> Reply.by_user()
      Keyword.has_key?(opts, :topic) -> opts |> Keyword.get(:topic) |> Reply.by_topic()
      true -> Reply
    end
  end
end


================================================
FILE: lib/mipha/replies/replies.ex
================================================
defmodule Mipha.Replies do
  @moduledoc """
  The Replies context.
  """

  import Ecto.Query, warn: false
  alias Ecto.Multi

  alias Mipha.{
    Repo,
    Topics,
    Notifications,
    Replies,
    Accounts
  }

  alias Replies.Reply
  alias Topics.Topic
  alias Accounts.User
  alias Mipha.Follows.Follow

  @username_regex ~r{@([A-Za-z0-9]+)}

  @doc """
  Returns the list of repies.

  ## Examples

      iex> list_repies()
      [%Reply{}, ...]

  """
  def list_repies do
    Repo.all(Reply)
  end

  @doc """
  Gets a single reply.

  Raises `Ecto.NoResultsError` if the Reply does not exist.

  ## Examples

      iex> get_reply!(123)
      %Reply{}

      iex> get_reply!(456)
      ** (Ecto.NoResultsError)

  """
  def get_reply!(id), do: Repo.get!(Reply, id)

  @doc """
  Creates a reply.

  ## Examples

      iex> create_reply(%{field: value})
      {:ok, %Reply{}}

      iex> create_reply(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_reply(attrs \\ %{}) do
    %Reply{}
    |> Reply.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a reply.

  ## Examples

      iex> update_reply(reply, %{field: new_value})
      {:ok, %Reply{}}

      iex> update_reply(reply, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_reply(%Reply{} = reply, attrs) do
    reply
    |> Reply.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Reply.

  ## Examples

      iex> delete_reply(reply)
      {:ok, %Reply{}}

      iex> delete_reply(reply)
      {:error, %Ecto.Changeset{}}

  """
  def delete_reply(%Reply{} = reply) do
    Multi.new()
    |> Multi.delete(:reply, reply)
    |> decrease_topic_reply_count()
    |> Repo.transaction()
  end

  defp decrease_topic_reply_count(multi) do
    update_topic_fn = fn _repo, %{reply: reply} ->
      topic =
        reply
        |> Reply.preload_topic()
        |> Map.fetch!(:topic)

      attrs = %{reply_count: topic.reply_count - 1}
      Topics.update_topic(topic, attrs)
    end

    Multi.run(multi, :decrease_topic_reply_count, update_topic_fn)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking reply changes.

  ## Examples

      iex> change_reply(reply)
      %Ecto.Changeset{source: %Reply{}}

  """
  def change_reply(%Reply{} = reply) do
    Reply.changeset(reply, %{})
  end

  @doc """
  Get the reply count of a replyable.

  ## Examples

      iex> get_reply_count(user: %User{})
      10

      iex> get_reply_count(topic: %Topic{})
      2

  """
  @spec get_reply_count(Keyword.t()) :: non_neg_integer()
  def get_reply_count(clauses) do
    query =
      clauses
      |> get_replyable_from_clauses()
      |> case do
        %Topic{} = topic -> Reply.by_topic(topic)
        %Reply{} = reply -> Reply.by_parent(reply)
        %User{} = user -> Reply.by_user(user)
      end

    query
    |> join(:inner, [r], u in assoc(r, :user))
    |> Repo.aggregate(:count, :id)
  end

  @doc """
  Gets all reply count.
  """
  @spec get_total_reply_count :: non_neg_integer()
  def get_total_reply_count do
    Reply
    |> Repo.aggregate(:count, :id)
  end

  defp get_replyable_from_clauses(clauses) do
    cond do
      Keyword.has_key?(clauses, :user) -> Keyword.get(clauses, :user)
      Keyword.has_key?(clauses, :topic) -> Keyword.get(clauses, :topic)
      Keyword.has_key?(clauses, :reply) -> Keyword.get(clauses, :reply)
    end
  end

  @doc """
  Return the recent of topics.
  """
  @spec recent_replies(User.t()) :: [Topic.t()] | nil
  def recent_replies(%User{} = user) do
    user
    |> Reply.by_user()
    |> Reply.recent()
    |> Repo.all()
    |> Repo.preload([:topic, :user])
  end

  @doc """
  Inserts a reply.

  ## Examples

      iex> insert_reply(%User{}, %{field: value})
      {:ok, %Reply{}}

      iex> insert_reply(%User{}, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  @spec insert_reply(User.t(), map()) :: {:ok, Reply.t()} | {:error, Ecto.Changeset.t()}
  def insert_reply(user, attrs \\ %{}) do
    attrs = attrs |> Map.put("user_id", user.id)
    reply_changeset = Reply.changeset(%Reply{}, attrs)

    Multi.new()
    |> Multi.insert(:reply, reply_changeset)
    |> update_related_topic()
    |> maybe_notify_follower_of_new_reply()
    |> notify_topic_owner_of_new_reply()
    |> maybe_notify_mention_users_of_new_reply(attrs)
    |> maybe_notify_parent_reply_owner_of_new_reply(attrs)
    |> Repo.transaction()
  end

  # Notification mention @ users.
  def maybe_notify_mention_users_of_new_reply(multi, attrs) do
    insert_notification_fn = fn %{reply: reply} ->
      notified_users =
        @username_regex
        |> Regex.scan(attrs["content"])
        |> Enum.map(fn [_, match] -> Accounts.get_user_by_username(match) end)
        |> Enum.filter(&(not is_nil(&1)))

      attrs = %{
        actor_id: reply.user_id,
        action: "reply_mentioned",
        reply_id: reply.id,
        notified_users: notified_users
      }

      case Notifications.insert_notification(attrs) do
        {:ok, %{notification: notification}} -> {:ok, notification}
        {:error, _, reason, _} -> {:error, reason}
      end
    end

    Multi.run(multi, :notify_mention_users_of_new_reply, insert_notification_fn)
  end

  # Update assoc topic.
  defp update_related_topic(multi) do
    update_topic_fn = fn %{reply: reply} ->
      topic =
        reply
        |> Reply.preload_topic()
        |> Map.fetch!(:topic)

      attrs = %{
        last_reply_id: reply.id,
        last_reply_user_id: reply.user_id,
        reply_count: topic.reply_count + 1
      }

      case Topics.update_topic(topic, attrs) do
        {:ok, topic} -> {:ok, topic}
        {:error, reason} -> {:error, reason}
      end
    end

    Multi.run(multi, :update_related_topic, update_topic_fn)
  end

  # Notificaton topic owner.
  defp notify_topic_owner_of_new_reply(multi) do
    insert_notification_fn = fn %{reply: reply} ->
      # reply -> topic -> user
      notified_users =
        reply
        |> Reply.preload_topic()
        |> Map.fetch!(:topic)
        |> Topic.preload_user()
        |> Map.fetch!(:user)

      attrs = %{
        actor_id: reply.user_id,
        action: "topic_reply_added",
        reply_id: reply.id,
        notified_users: [notified_users]
      }

      case Notifications.insert_notification(attrs) do
        {:ok, %{notification: notification}} -> {:ok, notification}
        {:error, _, reason, _} -> {:error, reason}
      end
    end

    Multi.run(multi, :notify_topic_owner_of_new_reply, insert_notification_fn)
  end

  # 如果是回复其他人的评论,回复该评论的作者, 有新的回复。
  defp maybe_notify_parent_reply_owner_of_new_reply(multi, %{"parent_id" => parent_id})
       when parent_id != "" do
    insert_notification_fn = fn %{reply: reply} ->
      notified_users =
        reply
        |> Reply.preload_parent()
        |> Map.fetch!(:parent)
        |> Reply.preload_user()
        |> Map.fetch!(:user)

      attrs = %{
        actor_id: reply.user_id,
        action: "reply_comment_added",
        reply_id: reply.id,
        notified_users: [notified_users]
      }

      case Notifications.insert_notification(attrs) do
        {:ok, %{notification: notification}} -> {:ok, notification}
        {:error, _, reason, _} -> {:error, reason}
      end
    end

    Multi.run(multi, :notify_parent_reply_owner_of_new_reply, insert_notification_fn)
  end

  defp maybe_notify_parent_reply_owner_of_new_reply(multi, _), do: multi

  # 发起评论时,通知关注评论作者的 follower
  defp maybe_notify_follower_of_new_reply(multi) do
    insert_notification_fn = fn %{reply: reply} ->
      # 取关注评论作者的 follower.
      notified_users = notifiable_users_of_reply(reply)

      attrs = %{
        actor_id: reply.user_id,
        action: "topic_reply_added",
        reply_id: reply.id,
        notified_users: notified_users
      }

      case Notifications.insert_notification(attrs) do
        {:ok, %{notification: notification}} -> {:ok, notification}
        {:error, _, reason, _} -> {:error, reason}
      end
    end

    Multi.run(multi, :notify_users_of_new_reply, insert_notification_fn)
  end

  defp notifiable_users_of_reply(%Reply{} = reply) do
    query =
      from u in User,
        join: f in Follow,
        on: f.follower_id == u.id,
        where: f.user_id == ^reply.user_id

    Repo.all(query)
  end

  @doc """
  Gets reply author.

  ## Example

      iex> author(%Topic{})
      %User{}

  """
  @spec author(Reply.t()) :: User.t()
  def author(%Reply{} = reply) do
    reply
    |> Reply.preload_user()
    |> Map.fetch!(:user)
  end
end


================================================
FILE: lib/mipha/replies/reply.ex
================================================
defmodule Mipha.Replies.Reply do
  @moduledoc false

  use Ecto.Schema
  import Ecto.{Changeset, Query}

  alias Mipha.{
    Repo,
    Topics.Topic,
    Replies.Reply,
    Accounts.User,
    Stars.Star
  }

  @type t :: %Reply{}

  schema "replies" do
    field :content, :string
    field :star_count, :integer, default: 0

    belongs_to :user, User
    belongs_to :topic, Topic
    belongs_to :parent, Reply, foreign_key: :parent_id

    has_many :children, Reply, foreign_key: :parent_id
    has_many :stars, Star

    timestamps()
  end

  @doc """
  Returns the children node.
  """
  @spec is_child(Ecto.Queryable.t()) :: Ecto.Query.t()
  def is_child(query \\ __MODULE__),
    do: from(q in query, where: not is_nil(q.parent_id))

  @doc """
  Filters the topic by reply.
  """
  @spec by_topic(Ecto.Queryable.t(), Topic.t()) :: Ecto.Query.t()
  def by_topic(query \\ __MODULE__, %Topic{id: topic_id}),
    do: where(query, [..., r], r.topic_id == ^topic_id)

  @doc """
  Filters the parent by reply.
  """
  @spec by_parent(Ecto.Queryable.t(), t()) :: Ecto.Query.t()
  def by_parent(query \\ __MODULE__, %__MODULE__{id: parent_id}),
    do: where(query, [..., r], r.parent_id == ^parent_id)

  @doc """
  Filters reply by user.
  """
  @spec by_user(Ecto.Queryable.t(), User.t()) :: Ecto.Query.t()
  def by_user(query \\ __MODULE__, %User{id: user_id}),
    do: where(query, [..., r], r.user_id == ^user_id)

  @doc """
  Get the recent of replies.
  """
  @spec recent(Ecto.Queryable.t()) :: Ecto.Query.t()
  def recent(query \\ __MODULE__),
    do: from(r in query, order_by: [desc: r.id], limit: 10)

  @doc """
  Preloads the user of a reply.
  """
  @spec preload_user(t()) :: t()
  def preload_user(reply), do: Repo.preload(reply, [:user])

  @doc """
  Preloads the topic of a reply.
  """
  @spec preload_topic(t()) :: t()
  def preload_topic(reply), do: Repo.preload(reply, [:topic])

  @doc """
  Preloads the parent of a reply.
  """
  @spec preload_parent(t()) :: t()
  def preload_parent(reply), do: Repo.preload(reply, [:parent])

  @doc false
  def changeset(reply, attrs) do
    permitted_attrs = ~w(
      star_count
      topic_id
      user_id
      parent_id
      content
    )a

    required_attrs = ~w(
      content
      user_id
      topic_id
    )a

    reply
    |> cast(attrs, permitted_attrs)
    |> validate_required(required_attrs)
  end
end


================================================
FILE: lib/mipha/repo.ex
================================================
defmodule Mipha.Repo do
  use Ecto.Repo, otp_app: :mipha, adapter: Ecto.Adapters.Postgres

  @doc """
  Dynamically loads the repository url from the
  DATABASE_URL environment variable.
  """
  def init(_, opts) do
    {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
  end
end


================================================
FILE: lib/mipha/stars/star.ex
================================================
defmodule Mipha.Stars.Star do
  @moduledoc false

  use Ecto.Schema
  import Ecto.{Changeset, Query}

  alias Mipha.{
    Repo,
    Topics.Topic,
    Replies.Reply,
    Accounts.User,
    Stars.Star
  }

  @type t :: %Star{}

  schema "stars" do
    belongs_to :reply, Reply
    belongs_to :user, User
    belongs_to :topic, Topic

    timestamps()
  end

  @doc """
  Filters the topic by star.
  """
  @spec by_topic(Ecto.Queryable.t(), Topic.t()) :: Ecto.Query.t()
  def by_topic(query \\ __MODULE__, %Topic{id: topic_id}),
    do: from(s in query, where: s.topic_id == ^topic_id)

  @doc """
  Filters the reply by star.
  """
  @spec by_reply(Ecto.Queryable.t(), Reply.t()) :: Ecto.Query.t()
  def by_reply(query \\ __MODULE__, %Reply{id: reply_id}),
    do: from(s in query, where: s.reply_id == ^reply_id)

  @doc """
  Preloads the reply of a topic.
  """
  @spec preload_reply(t()) :: t()
  def preload_reply(star), do: Repo.preload(star, :reply)

  @doc """
  Preloads the reply of a topic.
  """
  @spec preload_topic(t()) :: t()
  def preload_topic(star), do: Repo.preload(star, :topic)

  @doc false
  def changeset(star, attrs) do
    permitted_attrs = ~w(
      user_id
      reply_id
      topic_id
    )a

    required_attrs = ~w(
      user_id
    )a

    star
    |> cast(attrs, permitted_attrs)
    |> validate_required(required_attrs)
  end
end


================================================
FILE: lib/mipha/stars/stars.ex
================================================
defmodule Mipha.Stars do
  @moduledoc """
  The Stars context.
  """

  import Ecto.Query, warn: false
  alias Ecto.Multi

  alias Mipha.{
    Repo,
    Stars,
    Topics,
    Replies,
    Accounts,
    Notifications
  }

  alias Stars.Star
  alias Topics.Topic
  alias Replies.Reply
  alias Accounts.User

  @type starrable :: Topic.t() | Reply.t()

  @doc """
  Returns the list of stars.

  ## Examples

      iex> list_stars()
      [%Star{}, ...]

  """
  def list_stars do
    Repo.all(Star)
  end

  @doc """
  Gets a single star.

  Raises `Ecto.NoResultsError` if the Star does not exist.

  ## Examples

      iex> get_star!(123)
      %Star{}

      iex> get_star!(456)
      ** (Ecto.NoResultsError)

  """
  def get_star!(id), do: Repo.get!(Star, id)

  @doc """
  Creates a star.

  ## Examples

      iex> create_star(%{field: value})
      {:ok, %Star{}}

      iex> create_star(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_star(attrs \\ %{}) do
    %Star{}
    |> Star.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a star.

  ## Examples

      iex> update_star(star, %{field: new_value})
      {:ok, %Star{}}

      iex> update_star(star, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_star(%Star{} = star, attrs) do
    star
    |> Star.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Star.

  ## Examples

      iex> delete_star(star)
      {:ok, %Star{}}

      iex> delete_star(star)
      {:error, %Ecto.Changeset{}}

  """
  @spec delete_star(Star.t()) :: {:ok, Star.t()} | nil
  def delete_star(%Star{} = star) do
    Repo.delete(star)
  end

  @spec delete_star(Keyword.t()) :: {:ok, Star.t()} | nil
  def delete_star(clauses) do
    Multi.new()
    |> Multi.delete(:star, get_star(clauses))
    |> decrease_related_count(clauses)
    |> Repo.transaction()
  end

  # unstar, decrease topic star_count
  defp decrease_related_count(multi, user_id: _, topic_id: topic_id) when topic_id != "" do
    update_topic_fn = fn _repo, %{star: star} ->
      topic = starrable(star)
      attrs = %{star_count: topic.star_count - 1}
      Topics.update_topic(topic, attrs)
    end

    Multi.run(multi, :decrease_related_count, update_topic_fn)
  end

  # unstar, decrease reply star_count
  defp decrease_related_count(multi, user_id: _, reply_id: reply_id) when reply_id != "" do
    update_reply_fn = fn _repo, %{star: star} ->
      reply = starrable(star)
      attrs = %{star_count: reply.star_count - 1}
      Replies.update_reply(reply, attrs)
    end

    Multi.run(multi, :decrease_related_count, update_reply_fn)
  end

  defp decrease_related_count(multi, _), do: multi

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking star changes.

  ## Examples

      iex> change_star(star)
      %Ecto.Changeset{source: %Star{}}

  """
  def change_star(%Star{} = star) do
    Star.changeset(star, %{})
  end

  @doc """
  Gets a star.

  ## Examples

      iex> get_star(user_id: 123, topic_id: 123)
      %Star{}

      iex> get_star(user_id: 123, topic_id: 456)
      nil

  """
  @spec get_star(Keyword.t()) :: Star.t() | nil
  def get_star(clauses) when length(clauses) == 2, do: Repo.get_by(Star, clauses)

  @doc """
  Returns the starrable of the star.

  ## Examples

      iex> starrable(%Star{})
      %Topic{}

      iex> starrable(%Star{})
      %Reply{}

  """
  @spec starrable(Star.t()) :: Topic.t() | Reply.t()
  def starrable(%Star{topic_id: topic_id} = star) when not is_nil(topic_id) do
    star
    |> Star.preload_topic()
    |> Map.fetch!(:topic)
  end

  def starrable(%Star{reply_id: reply_id} = star) when not is_nil(reply_id) do
    star
    |> Star.preload_reply()
    |> Map.fetch!(:reply)
  end

  @doc """
  Inserts a star.
  """
  # @spec insert_star(map()) :: {:ok, Star.t()} | {:error, }
  def insert_star(attrs) do
    start_changeset = Star.changeset(%Star{}, attrs)

    Multi.new()
    |> Multi.insert(:star, start_changeset)
    |> increase_related_count(attrs)
    |> notify_author_of_starrable()
    |> Repo.transaction()
  end

  # star, increase topic star_count
  defp increase_related_count(multi, %{topic_id: topic_id}) when topic_id != "" do
    update_topic_fn = fn _repo, %{star: star} ->
      topic = starrable(star)
      attrs = %{star_count: topic.star_count + 1}
      Topics.update_topic(topic, attrs)
    end

    Multi.run(multi, :increase_related_count, update_topic_fn)
  end

  # star, increase reply star_count
  defp increase_related_count(multi, %{reply_id: reply_id}) when reply_id != "" do
    update_reply_fn = fn %{star: star} ->
      reply = starrable(star)
      attrs = %{star_count: reply.star_count + 1}
      Replies.update_reply(reply, attrs)
    end

    Multi.run(multi, :increase_related_count, update_reply_fn)
  end

  defp increase_related_count(multi, _), do: multi

  defp notify_author_of_starrable(multi) do
    insert_notification_fn = fn _repo, %{star: star} ->
      starrable = starrable(star)

      author =
        case starrable do
          %Topic{} = topic -> Topics.author(topic)
          %Reply{} = reply -> Replies.author(reply)
        end

      action =
        case starrable do
          %Topic{} -> "topic_starred"
          %Reply{} -> "reply_starred"
        end

      notified_users = [author]

      notification_attrs =
        starrable
        |> case do
          %Topic{id: topic_id} -> %{topic_id: topic_id}
          %Reply{id: reply_id} -> %{reply_id: reply_id}
        end
        |> Map.merge(%{
          actor_id: author.id,
          action: action,
          notified_users: notified_users
        })

      case Notifications.insert_notification(notification_attrs) do
        {:ok, %{notification: notification}} -> {:ok, notification}
        {:error, _, reason, _} -> {:error, reason}
      end
    end

    Multi.run(multi, :notification, insert_notification_fn)
  end

  # defp starrable_author(%Topic{} = topic), do: Topics.author(topic)
  # defp starrable_author(%Reply{} = reply), do: Replies.author(reply)

  @doc """
  Returns `true` if the user has starred the starrable.
  `false` otherwise.

  ## Examples

      iex> has_starred?(user: %User{}, topic: %Topic{})
      true

      iex> has_starred?(user: %User{}, topic: %Topic{})
      false

  """
  @spec has_starred?(Keyword.t()) :: boolean
  def has_starred?(clauses) do
    %User{id: user_id} = Keyword.get(clauses, :user)

    clauses =
      clauses
      |> get_starrable_from_clauses()
      |> case do
        %Topic{id: topic_id} -> [topic_id: topic_id]
        %Reply{id: reply_id} -> [reply_id: reply_id]
      end
      |> Keyword.put(:user_id, user_id)

    !!get_star(clauses)
  end

  @spec get_starrable_from_clauses(Keyword.t()) :: starrable()
  defp get_starrable_from_clauses(clauses) do
    cond do
      Keyword.has_key?(clauses, :topic) -> Keyword.get(clauses, :topic)
      Keyword.has_key?(clauses, :reply) -> Keyword.get(clauses, :reply)
      true -> Star
    end
  end
end


================================================
FILE: lib/mipha/token.ex
================================================
defmodule Mipha.Token do
  @moduledoc false

  alias Mipha.Accounts.User

  @verification_salt "mipha"

  def generate_token(%User{id: user_id}) do
    Phoenix.Token.sign(MiphaWeb.Endpoint, @verification_salt, user_id)
  end

  def verify_token(token) do
    max_age = 86_400
    Phoenix.Token.verify(MiphaWeb.Endpoint, @verification_salt, token, max_age: max_age)
  end
end


================================================
FILE: lib/mipha/topics/node.ex
================================================
defmodule Mipha.Topics.Node do
  @moduledoc false
  use Ecto.Schema
  import Ecto.{Changeset, Query}

  alias Mipha.Repo
  alias Mipha.Topics.{Topic, Node}

  @type t :: %Node{}

  schema "nodes" do
    field :name, :string
    field :position, :integer
    field :summary, :string

    belongs_to :parent, Node, foreign_key: :parent_id

    has_many :topics, Topic
    has_many :children, Node, foreign_key: :parent_id

    timestamps()
  end

  @doc """
  Returns the children node.
  """
  @spec is_child(Ecto.Queryable.t()) :: Ecto.Query.t()
  def is_child(query \\ __MODULE__), do: from(q in query, where: not is_nil(q.parent_id))

  @doc """
  Returns the parent of node.
  """
  @spec is_parent(Ecto.Queryable.t()) :: Ecto.Query.t()
  def is_parent(query \\ __MODULE__), do: from(q in query, where: is_nil(q.parent_id))

  @doc """
  Preloads the children of a node.
  """
  @spec preload_children(t()) :: t()
  def preload_children(node), do: Repo.preload(node, :children)

  @doc false
  def changeset(node, attrs) do
    permitted_attrs = ~w(
      name
      summary
      position
      parent_id
    )a

    required_attrs = ~w(
      name
    )a

    node
    |> cast(attrs, permitted_attrs)
    |> validate_required(required_attrs)
  end
end


================================================
FILE: lib/mipha/topics/queries.ex
================================================
defmodule Mipha.Topics.Queries do
  @moduledoc false

  import Ecto.Query
  alias Mipha.Topics.{Topic, Node}

  @doc """
  Returns the list topics.
  """
  @spec list_topics :: Ecto.Query.t()
  def list_topics, do: Topic

  @doc """
  Returns the list nodes.
  """
  @spec list_nodes :: Ecto.Query.t()
  def list_nodes, do: Node

  @doc """
  Returns the the type equal job topics.
  """
  @spec job_topics :: Ecto.Query.t()
  def job_topics do
    Topic.job()
    |> preload([:user, :node, :last_reply_user])
  end

  @doc """
  Filter the list of topics.

  ## Examples

      iex> cond_topics()
      Ecto.Query.t()

      iex> cond_topics(type: :educational)
      Ecto.Query.t()

      iex> cond_topics(node: %Node{})
      Ecto.Query.t()

      iex> cond_topics(user: %User{})
      Ecto.Query.t()

  """
  @spec cond_topics(Keyword.t()) :: Ecto.Query.t()
  def cond_topics(opts) do
    opts
    |> filter_from_clauses
    |> Topic.base_order()
    |> preload([:user, :node, :last_reply_user])
  end

  defp filter_from_clauses(opts), do: do_filter_from_clauses(opts)

  defp do_filter_from_clauses(type: :educational), do: Topic.educational()
  defp do_filter_from_clauses(type: :featured), do: Topic.featured()
  defp do_filter_from_clauses(type: :no_reply), do: Topic.no_reply()
  defp do_filter_from_clauses(type: :popular), do: Topic.popular()
  defp do_filter_from_clauses(node: node), do: Topic.by_node(node)
  defp do_filter_from_clauses(user: user), do: Topic.by_user(user)
  defp do_filter_from_clauses(user_ids: user_ids), do: Topic.by_user_ids(user_ids)
  defp do_filter_from_clauses(_), do: Topic
end


================================================
FILE: lib/mipha/topics/topic.ex
================================================
defmodule Mipha.Topics.Topic do
  @moduledoc false

  use Ecto.Schema
  import Ecto.{Changeset, Query}
  import EctoEnum, only: [defenum: 3]

  alias Mipha.{
    Repo,
    Accounts.User,
    Replies.Reply,
    Stars.Star,
    Collections.Collection
  }

  alias Mipha.Topics.{Topic, Node}

  @type t :: %Topic{}

  defenum(TopicType, :topic_type, [
    :normal,
    :featured,
    :educational,
    :job
  ])

  schema "topics" do
    field :title, :string
    field :body, :string
    field :type, TopicType
    field :closed_at, :naive_datetime
    field :replied_at, :naive_datetime
    field :suggested_at, :naive_datetime
    field :reply_count, :integer, default: 0
    field :visit_count, :integer, default: 0
    field :star_count, :integer, default: 0

    belongs_to :user, User
    belongs_to :node, Node
    belongs_to :last_reply, Reply, foreign_key: :last_reply_id
    belongs_to :last_reply_user, User, foreign_key: :last_reply_user_id

    has_many :replies, Reply
    has_many :stars, Star
    has_many :collections, Collection, on_delete: :delete_all

    timestamps()
  end

  @doc """
  Returns the job of topic.
  """
  @spec job(Ecto.Queryable.t()) :: Ecto.Query.t()
  def job(query \\ __MODULE__),
    do: where(query, [..., t], t.type == ^:job)

  @doc """
  Filters the featured of topics.
  """
  @spec featured(Ecto.Queryable.t()) :: Ecto.Query.t()
  def featured(query \\ __MODULE__),
    do: where(query, [..., t], t.type == ^:featured)

  @doc """
  Filters the educational of topics.
  """
  @spec educational(Ecto.Queryable.t()) :: Ecto.Query.t()
  def educational(query \\ __MODULE__),
    do: where(query, [..., t], t.type == ^:educational)

  @doc """
  Filters the no_reply of topics.
  """
  @spec no_reply(Ecto.Queryable.t()) :: Ecto.Query.t()
  def no_reply(query \\ __MODULE__),
    do: where(query, [..., t], t.reply_count == 0)

  @doc """
  Filters the popular of topics.
  """
  @spec popular(Ecto.Queryable.t()) :: Ecto.Query.t()
  def popular(query \\ __MODULE__),
    do: where(query, [..., t], t.reply_count >= 10)

  @doc """
  Filters the node of topics.
  """
  @spec by_node(Ecto.Queryable.t(), Node.t()) :: Ecto.Query.t()
  def by_node(query \\ __MODULE__, %Node{id: node_id}),
    do: where(query, [..., t], t.node_id == ^node_id)

  @doc """
  Filters the user of topics.
  """
  @spec by_user(Ecto.Queryable.t(), User.t()) :: Ecto.Query.t()
  def by_user(query \\ __MODULE__, %User{id: user_id}),
    do: where(query, [..., t], t.user_id == ^user_id)

  @doc """
  Filters the user of topics.
  """
  @spec by_user_ids(Ecto.Queryable.t(), List.t()) :: Ecto.Query.t()
  def by_user_ids(query \\ __MODULE__, list),
    do: where(query, [..., t], t.user_id in ^list)

  @doc """
  Returns nearly 10 topics
  """
  @spec recent(t()) :: t()
  def recent(query \\ __MODULE__),
    do: from(t in query, order_by: [desc: t.updated_at], limit: 10)

  @doc """
  Returns the default sort of the topic list, according to suggested_at && updated_at
  """
  @spec base_order(t()) :: t()
  def base_order(query \\ __MODULE__),
    do: from(t in query, order_by: [asc: t.suggested_at], order_by: [desc: t.updated_at])

  @doc """
  Preloads the user of a topic.
  """
  @spec preload_user(t()) :: t()
  def preload_user(topic), do: Repo.preload(topic, [:user, :last_reply_user])

  @doc """
  Preloads the reply of a topic.
  """
  @spec preload_replies(t()) :: t()
  def preload_replies(topic), do: Repo.preload(topic, :replies)

  @doc """
  Preloads the reply of a topic.
  """
  @spec preload_node(t()) :: t()
  def preload_node(topic), do: Repo.preload(topic, :node)

  @doc """
  Preloads all of a topic.
  """
  @spec preload_all(t()) :: t()
  def preload_all(topic) do
    topic
    |> preload_replies
    |> preload_user
    |> preload_node
  end

  @doc """
  Topic increment/decrement count
  """
  def counter(%Topic{id: topic_id}, :inc, :visit_count) do
    Topic
    |> where([..., t], t.id == ^topic_id)
    |> Repo.update_all(inc: [visit_count: 1])
  end

  def counter(%Topic{id: topic_id}, :inc, :reply_count) do
    Topic
    |> where([..., t], t.id == ^topic_id)
    |> Repo.update_all(inc: [reply_count: 1])
  end

  def counter(%Topic{id: topic_id}, :dec, :reply_count) do
    Topic
    |> where([..., t], t.id == ^topic_id)
    |> Repo.update_all(inc: [reply_count: -1])
  end

  @doc false
  def changeset(topic, attrs) do
    permitted_attrs = ~w(
      title
      body
      closed_at
      user_id
      type
      node_id
      visit_count
      reply_count
      star_count
      last_reply_id
      last_reply_user_id
      replied_at
      suggested_at
    )a

    required_attrs = ~w(
      title
      body
      node_id
      user_id
    )a

    topic
    |> cast(attrs, permitted_attrs)
    |> validate_required(required_attrs)
  end
end


================================================
FILE: lib/mipha/topics/topics.ex
================================================
defmodule Mipha.Topics do
  @moduledoc """
  The Topics context.
  """

  import Ecto.Query, warn: false
  alias Ecto.Multi

  alias Mipha.{
    Repo,
    Accounts,
    Notifications
  }

  alias Mipha.Topics.Topic
  alias Accounts.User
  alias Mipha.Follows.Follow

  @username_regex ~r{@([A-Za-z0-9]+)}

  @doc """
  Returns the list of topics.

  ## Examples

      iex> list_topics()
      [%Topic{}, ...]

  """
  def list_topics do
    Topic
    |> Repo.all()
  end

  @doc """
  Gets a single topic.

  Raises `Ecto.NoResultsError` if the Topic does not exist.

  ## Examples

      iex> get_topic!(123)
      %Topic{}

      iex> get_topic!(456)
      ** (Ecto.NoResultsError)

  """
  def get_topic!(id) do
    Topic
    |> Repo.get!(id)
    |> Repo.preload([:node, :user, :last_reply_user, [replies: [:user, [parent: :user]]]])
  end

  @doc """
  Creates a topic.

  ## Examples

      iex> create_topic(%{field: value})
      {:ok, %Topic{}}

      iex> create_topic(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_topic(attrs \\ %{}) do
    %Topic{}
    |> Topic.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a topic.

  ## Examples

      iex> update_topic(topic, %{field: new_value})
      {:ok, %Topic{}}

      iex> update_topic(topic, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_topic(%Topic{} = topic, attrs) do
    topic
    |> Topic.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Topic.

  ## Examples

      iex> delete_topic(topic)
      {:ok, %Topic{}}

      iex> delete_topic(topic)
      {:error, %Ecto.Changeset{}}

  """
  def delete_topic(%Topic{} = topic) do
    Repo.delete(topic)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking topic changes.

  ## Examples

      iex> change_topic(topic)
      %Ecto.Changeset{source: %Topic{}}

  """
  @spec change_topic(Topic.t()) :: Ecto.Changeset.t()
  def change_topic(%Topic{} = topic \\ %Topic{}), do: Topic.changeset(topic, %{})

  @doc """
  Increment topic visit count.
  """
  def topic_visit_counter(%Topic{} = topic), do: Topic.counter(topic, :inc, :visit_count)

  @doc """
  Inserts a topic.

  ## Examples

      iex> insert_topic(%User{}, %{field: value})
      {:ok, %Topic{}}

      iex> insert_topic(%User{}, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  @spec insert_topic(User.t(), map()) :: {:ok, Topic.t()} | {:error, Ecto.Changeset.t()}
  def insert_topic(user, attrs \\ %{}) do
    attrs = attrs |> Map.put("user_id", user.id)
    topic_changeset = Topic.changeset(%Topic{}, attrs)

    Multi.new()
    |> Multi.insert(:topic, topic_changeset)
    |> maybe_notify_users_of_new_topic()
    |> maybe_notify_mention_users_of_new_topic(attrs)
    |> Repo.transaction()
  end

  defp maybe_notify_users_of_new_topic(multi) do
    insert_notification_fn = fn _repo, %{topic: topic} ->
      # 获取关注话题作者的 follower.
      notified_users = notifiable_users_of_topic(topic)

      attrs = %{
        actor_id: topic.user_id,
        action: "topic_added",
        topic_id: topic.id,
        notified_users: notified_users
      }

      case Notifications.insert_notification(attrs) do
        {:ok, %{notification: notification}} -> {:ok, notification}
        {:error, _, reason, _} -> {:error, reason}
      end
    end

    Multi.run(multi, :notify_users_of_new_topic, insert_notification_fn)
  end

  # 通知被 @ 的用户
  def maybe_notify_mention_users_of_new_topic(multi, attrs) do
    insert_notification_fn = fn _repo, %{topic: topic} ->
      notified_users =
        @username_regex
        |> Regex.scan(attrs["body"])
        |> Enum.map(fn [_, match] -> Accounts.get_user_by_username(match) end)
        |> Enum.filter(&(not is_nil(&1)))

      attrs = %{
        actor_id: topic.user_id,
        action: "topic_mentioned",
        topic_id: topic.id,
        notified_users: notified_users
      }

      case Notifications.insert_notification(attrs) do
        {:ok, %{notification: notification}} -> {:ok, notification}
        {:error, _, reason, _} -> {:error, reason}
      end
    end

    Multi.run(multi, :notify_mention_users_of_new_topic, insert_notification_fn)
  end

  # 获取关注话题作者的 follower.
  def notifiable_users_of_topic(%Topic{} = topic) do
    query =
      from u in User,
        join: f in Follow,
        on: f.follower_id == u.id,
        where: f.user_id == ^topic.user_id

    Repo.all(query)
  end

  @doc """
  Returns the featured of topics.

  ## Examples

      iex> list_topics()
      [%Topic{}, ...]

  """
  def list_featured_topics do
    Topic.featured()
    |> Repo.all()
    |> Repo.preload([:node, :user, :last_reply_user])
  end

  @doc """
  Return topic count of a owner(user).
  """
  @spec get_topic_count(User.t()) :: non_neg_integer()
  def get_topic_count(%User{} = user) do
    Topic
    |> Topic.by_user(user)
    |> Repo.aggregate(:count, :id)
  end

  @doc """
  获取全部 topic 个数
  """
  @spec get_total_topic_count :: non_neg_integer()
  def get_total_topic_count do
    Topic
    |> Repo.aggregate(:count, :id)
  end

  @doc """
  Return the recent of topics.
  """
  @spec recent_topics(User.t()) :: [Topic.t()] | nil
  def recent_topics(%User{} = user) do
    user
    |> Topic.by_user()
    |> Topic.recent()
    |> Repo.all()
    |> Repo.preload([:node, :user, :last_reply_user])
  end

  @doc """
  获取话题作者

  ## Example

      iex> author(%Topic{})
      %User{}

  """
  @spec author(Topic.t()) :: User.t()
  def author(%Topic{} = topic) do
    topic
    |> Topic.preload_user()
    |> Map.fetch!(:user)
  end

  alias Mipha.Topics.Node

  @doc """
  Returns the list of nodes.

  ## Examples

      iex> list_nodes()
      [%Node{}, ...]

  """
  def list_nodes do
    Repo.all(Node)
  end

  @doc """
  Gets a single node.

  Raises `Ecto.NoResultsError` if the Node does not exist.

  ## Examples

      iex> get_node!(123)
      %Node{}

      iex> get_node!(456)
      ** (Ecto.NoResultsError)

  """
  def get_node!(id), do: Repo.get!(Node, id)

  @doc """
  Creates a node.

  ## Examples

      iex> create_node(%{field: value})
      {:ok, %Node{}}

      iex> create_node(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_node(attrs \\ %{}) do
    %Node{}
    |> Node.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a node.

  ## Examples

      iex> update_node(node, %{field: new_value})
      {:ok, %Node{}}

      iex> update_node(node, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_node(%Node{} = node, attrs) do
    node
    |> Node.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a N
Download .txt
gitextract_xfkpu7cw/

├── .coveralls.yml
├── .credo.exs
├── .formatter.exs
├── .github/
│   └── workflows/
│       └── elixir.yml
├── .gitignore
├── .iex.exs
├── .travis.yml
├── LICENSE
├── Makefile
├── Procfile
├── README.ZH.md
├── README.md
├── assets/
│   ├── .eslintrc.json
│   ├── .yarnrc
│   ├── css/
│   │   ├── admin.scss
│   │   ├── app/
│   │   │   └── components/
│   │   │       ├── _footer.scss
│   │   │       ├── _notification.scss
│   │   │       ├── base.scss
│   │   │       ├── page.scss
│   │   │       └── user.scss
│   │   ├── app.scss
│   │   └── common/
│   │       ├── components/
│   │       │   └── _header.scss
│   │       └── variables/
│   │           └── mipha.scss
│   ├── js/
│   │   ├── admin.js
│   │   ├── app/
│   │   │   └── components/
│   │   │       ├── editor.js
│   │   │       ├── session.js
│   │   │       ├── times.js
│   │   │       └── topic.js
│   │   ├── app.js
│   │   ├── common/
│   │   │   └── components/
│   │   │       └── utils.js
│   │   └── socket.js
│   ├── package.json
│   ├── static/
│   │   └── robots.txt
│   └── webpack.config.js
├── compile
├── config/
│   ├── config.exs
│   ├── dev.exs
│   ├── prod.exs
│   └── test.exs
├── docker/
│   └── docker-compose.yml
├── elixir_buildpack.config
├── lib/
│   ├── mipha/
│   │   ├── accounts/
│   │   │   ├── accounts.ex
│   │   │   ├── company.ex
│   │   │   ├── location.ex
│   │   │   ├── queries.ex
│   │   │   ├── team.ex
│   │   │   ├── user.ex
│   │   │   └── user_team.ex
│   │   ├── application.ex
│   │   ├── collections/
│   │   │   ├── collection.ex
│   │   │   ├── collections.ex
│   │   │   └── queries.ex
│   │   ├── factory.ex
│   │   ├── follows/
│   │   │   ├── follow.ex
│   │   │   ├── follows.ex
│   │   │   └── queries.ex
│   │   ├── mailer.ex
│   │   ├── markdown/
│   │   │   ├── auto_linker.ex
│   │   │   ├── embed_video_replacer.ex
│   │   │   ├── emoji_replacer.ex
│   │   │   ├── html_renderer.ex
│   │   │   ├── markdown_guides.md
│   │   │   └── mention_replacer.ex
│   │   ├── markdown.ex
│   │   ├── notifications/
│   │   │   ├── notification.ex
│   │   │   ├── notifications.ex
│   │   │   ├── queries.ex
│   │   │   └── user_notification.ex
│   │   ├── qiniu.ex
│   │   ├── regexp.ex
│   │   ├── replies/
│   │   │   ├── queries.ex
│   │   │   ├── replies.ex
│   │   │   └── reply.ex
│   │   ├── repo.ex
│   │   ├── stars/
│   │   │   ├── star.ex
│   │   │   └── stars.ex
│   │   ├── token.ex
│   │   ├── topics/
│   │   │   ├── node.ex
│   │   │   ├── queries.ex
│   │   │   ├── topic.ex
│   │   │   └── topics.ex
│   │   └── utils/
│   │       └── store.ex
│   ├── mipha.ex
│   ├── mipha_web/
│   │   ├── channels/
│   │   │   ├── presence.ex
│   │   │   ├── room_channel.ex
│   │   │   ├── topic_channel.ex
│   │   │   └── user_socket.ex
│   │   ├── controllers/
│   │   │   ├── admin/
│   │   │   │   ├── company_controller.ex
│   │   │   │   ├── node_controller.ex
│   │   │   │   ├── notification_controller.ex
│   │   │   │   ├── page_controller.ex
│   │   │   │   ├── reply_controller.ex
│   │   │   │   ├── team_controller.ex
│   │   │   │   ├── topic_controller.ex
│   │   │   │   └── user_controller.ex
│   │   │   ├── auth_controller.ex
│   │   │   ├── callback_controller.ex
│   │   │   ├── company_controller.ex
│   │   │   ├── location_controller.ex
│   │   │   ├── notification_controller.ex
│   │   │   ├── page_controller.ex
│   │   │   ├── reply_controller.ex
│   │   │   ├── search_controller.ex
│   │   │   ├── session_controller.ex
│   │   │   ├── setting_controller.ex
│   │   │   ├── team_controller.ex
│   │   │   ├── topic_controller.ex
│   │   │   └── user_controller.ex
│   │   ├── email.ex
│   │   ├── endpoint.ex
│   │   ├── gettext.ex
│   │   ├── plugs/
│   │   │   ├── attack.ex
│   │   │   ├── current_user.ex
│   │   │   ├── locale.ex
│   │   │   ├── require_admin.ex
│   │   │   └── require_user.ex
│   │   ├── router.ex
│   │   ├── session.ex
│   │   ├── templates/
│   │   │   ├── admin/
│   │   │   │   ├── company/
│   │   │   │   │   └── index.html.eex
│   │   │   │   ├── node/
│   │   │   │   │   ├── edit.html.eex
│   │   │   │   │   ├── form.html.eex
│   │   │   │   │   ├── index.html.eex
│   │   │   │   │   ├── new.html.eex
│   │   │   │   │   └── show.html.eex
│   │   │   │   ├── notification/
│   │   │   │   │   ├── index.html.eex
│   │   │   │   │   └── show.html.eex
│   │   │   │   ├── page/
│   │   │   │   │   └── index.html.eex
│   │   │   │   ├── reply/
│   │   │   │   │   ├── index.html.eex
│   │   │   │   │   └── show.html.eex
│   │   │   │   ├── team/
│   │   │   │   │   ├── edit.html.eex
│   │   │   │   │   ├── form.html.eex
│   │   │   │   │   ├── index.html.eex
│   │   │   │   │   ├── new.html.eex
│   │   │   │   │   └── show.html.eex
│   │   │   │   ├── topic/
│   │   │   │   │   └── index.html.eex
│   │   │   │   └── user/
│   │   │   │       └── index.html.eex
│   │   │   ├── auth/
│   │   │   │   └── login.html.eex
│   │   │   ├── company/
│   │   │   │   ├── index.html.eex
│   │   │   │   └── show.html.eex
│   │   │   ├── email/
│   │   │   │   ├── forgot_password.html.eex
│   │   │   │   ├── verify_email.html.eex
│   │   │   │   └── welcome.html.eex
│   │   │   ├── layout/
│   │   │   │   ├── _flash.html.eex
│   │   │   │   ├── _footer.html.eex
│   │   │   │   ├── _header.html.eex
│   │   │   │   ├── _navbar.html.eex
│   │   │   │   ├── _sub_header.html.eex
│   │   │   │   ├── admin.html.eex
│   │   │   │   ├── app.html.eex
│   │   │   │   └── email.html.eex
│   │   │   ├── location/
│   │   │   │   ├── index.html.eex
│   │   │   │   └── show.html.eex
│   │   │   ├── notification/
│   │   │   │   ├── _followed.html.eex
│   │   │   │   ├── _reply_comment_added.html.eex
│   │   │   │   ├── _reply_mentioned.html.eex
│   │   │   │   ├── _reply_starred.html.eex
│   │   │   │   ├── _topic_added.html.eex
│   │   │   │   ├── _topic_mentioned.html.eex
│   │   │   │   ├── _topic_reply_added.html.eex
│   │   │   │   ├── _topic_starred.html.eex
│   │   │   │   └── index.html.eex
│   │   │   ├── page/
│   │   │   │   ├── _locations.html.eex
│   │   │   │   ├── index.html.eex
│   │   │   │   └── markdown.html.eex
│   │   │   ├── reply/
│   │   │   │   └── edit.html.eex
│   │   │   ├── search/
│   │   │   │   └── index.html.eex
│   │   │   ├── session/
│   │   │   │   └── new.html.eex
│   │   │   ├── setting/
│   │   │   │   ├── _sidebar.html.eex
│   │   │   │   ├── account.html.eex
│   │   │   │   ├── password.html.eex
│   │   │   │   ├── profile.html.eex
│   │   │   │   ├── reward.html.eex
│   │   │   │   └── show.html.eex
│   │   │   ├── shared/
│   │   │   │   └── _pagination.html.eex
│   │   │   ├── team/
│   │   │   │   ├── _nav.html.eex
│   │   │   │   ├── index.html.eex
│   │   │   │   ├── people.html.eex
│   │   │   │   └── show.html.eex
│   │   │   ├── topic/
│   │   │   │   ├── _editor_toolbar.html.eex
│   │   │   │   ├── _form.html.eex
│   │   │   │   ├── _handle_reply.html.eex
│   │   │   │   ├── _need_register_or_login.html.eex
│   │   │   │   ├── _node_selector.html.eex
│   │   │   │   ├── _nodes.html.eex
│   │   │   │   ├── _operate_toolbar.html.eex
│   │   │   │   ├── _relation_topic.html.eex
│   │   │   │   ├── _replies.html.eex
│   │   │   │   ├── _right.html.eex
│   │   │   │   ├── _right_sidebar.html.eex
│   │   │   │   ├── _topic.html.eex
│   │   │   │   ├── _topics.html.eex
│   │   │   │   ├── edit.html.eex
│   │   │   │   ├── educational.html.eex
│   │   │   │   ├── featured.html.eex
│   │   │   │   ├── index.html.eex
│   │   │   │   ├── jobs.html.eex
│   │   │   │   ├── new.html.eex
│   │   │   │   ├── no_reply.html.eex
│   │   │   │   ├── popular.html.eex
│   │   │   │   └── show.html.eex
│   │   │   └── user/
│   │   │       ├── _left.html.eex
│   │   │       ├── _menu.html.eex
│   │   │       ├── _repos.html.eex
│   │   │       ├── _reward.html.eex
│   │   │       ├── _teams.html.eex
│   │   │       ├── collections.html.eex
│   │   │       ├── followers.html.eex
│   │   │       ├── following.html.eex
│   │   │       ├── forgot_password.html.eex
│   │   │       ├── index.html.eex
│   │   │       ├── invalid_token.html.eex
│   │   │       ├── replies.html.eex
│   │   │       ├── reset_password.html.eex
│   │   │       ├── show.html.eex
│   │   │       ├── topics.html.eex
│   │   │       └── verified.html.eex
│   │   └── views/
│   │       ├── admin/
│   │       │   ├── company_view.ex
│   │       │   ├── node_view.ex
│   │       │   ├── notification_view.ex
│   │       │   ├── page_view.ex
│   │       │   ├── reply_view.ex
│   │       │   ├── team_view.ex
│   │       │   ├── topic_view.ex
│   │       │   └── user_view.ex
│   │       ├── auth_view.ex
│   │       ├── company_view.ex
│   │       ├── email_view.ex
│   │       ├── error_helpers.ex
│   │       ├── error_view.ex
│   │       ├── layout_view.ex
│   │       ├── location_view.ex
│   │       ├── notification_view.ex
│   │       ├── page_view.ex
│   │       ├── reply_view.ex
│   │       ├── search_view.ex
│   │       ├── session_view.ex
│   │       ├── setting_view.ex
│   │       ├── shared_view.ex
│   │       ├── tag_helpers.ex
│   │       ├── team_view.ex
│   │       ├── topic_view.ex
│   │       ├── user_view.ex
│   │       └── view_helpers.ex
│   └── mipha_web.ex
├── mix.exs
├── phoenix_static_buildpack.config
├── priv/
│   ├── gettext/
│   │   ├── default.pot
│   │   ├── en/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── default.po
│   │   │       └── errors.po
│   │   ├── errors.pot
│   │   └── zh/
│   │       └── LC_MESSAGES/
│   │           ├── default.po
│   │           └── errors.po
│   └── repo/
│       ├── migrations/
│       │   ├── 20180629095811_create_users.exs
│       │   ├── 20180702075249_create_topics.exs
│       │   ├── 20180702081217_create_replies.exs
│       │   ├── 20180702081845_create_nodes.exs
│       │   ├── 20180703094024_create_locations.exs
│       │   ├── 20180704024042_create_collections.exs
│       │   ├── 20180704024600_create_follows.exs
│       │   ├── 20180704025308_create_stars.exs
│       │   ├── 20180704032212_create_companies.exs
│       │   ├── 20180704053120_create_teams.exs
│       │   ├── 20180704053159_create_users_teams.exs
│       │   ├── 20180712015550_create_notifications.exs
│       │   └── 20180712015614_create_users_notifications.exs
│       └── seeds.exs
├── script/
│   ├── credo
│   ├── iex
│   ├── serve
│   └── setup
└── test/
    ├── mipha/
    │   ├── accounts/
    │   │   ├── accounts_test.exs
    │   │   ├── company_test.exs
    │   │   ├── location_test.exs
    │   │   ├── queries_test.exs
    │   │   └── user_test.exs
    │   ├── collections/
    │   │   ├── collection_test.exs
    │   │   ├── collections_test.exs
    │   │   └── queries_test.exs
    │   ├── follows/
    │   │   ├── follow_test.exs
    │   │   ├── follows_test.exs
    │   │   └── queries_test.exs
    │   ├── notifications/
    │   │   ├── notification_test.exs
    │   │   ├── notifications_test.exs
    │   │   ├── queries_test.exs
    │   │   └── user_notification_test.exs
    │   ├── replies/
    │   │   ├── queries_test.exs
    │   │   ├── replies_test.exs
    │   │   └── reply_test.exs
    │   ├── stars/
    │   │   ├── star_test.exs
    │   │   └── stars_test.exs
    │   └── topics/
    │       ├── node_test.exs
    │       ├── queries_test.exs
    │       ├── topic_test.exs
    │       └── topics_test.exs
    ├── mipha_web/
    │   ├── channels/
    │   │   ├── room_channel_test.exs
    │   │   └── topic_channel_test.exs
    │   ├── controllers/
    │   │   ├── admin/
    │   │   │   ├── company_controller_test.exs
    │   │   │   ├── node_controller_test.exs
    │   │   │   ├── page_controller_test.exs
    │   │   │   ├── reply_controller_test.exs
    │   │   │   ├── team_controller_test.exs
    │   │   │   ├── topic_controller_test.exs
    │   │   │   └── user_controller_test.exs
    │   │   ├── auth_controller_test.exs
    │   │   ├── location_controller_test.exs
    │   │   ├── notification_controller_test.exs
    │   │   ├── page_controller_test.exs
    │   │   ├── reply_controller_test.exs
    │   │   ├── session_controller_test.exs
    │   │   ├── setting_controller_test.exs
    │   │   ├── topic_controller_test.exs
    │   │   └── user_controller_test.exs
    │   └── views/
    │       ├── error_view_test.exs
    │       ├── layout_view_test.exs
    │       └── page_view_test.exs
    ├── support/
    │   ├── channel_case.ex
    │   ├── conn_case.ex
    │   └── data_case.ex
    └── test_helper.exs
Download .txt
SYMBOL INDEX (699 symbols across 170 files)

FILE: assets/js/app/components/editor.js
  class Editor (line 1) | class Editor {
    method constructor (line 2) | constructor() {
    method initDropzone (line 8) | initDropzone() {
    method browseUpload (line 58) | browseUpload() {
    method uploadFile (line 65) | uploadFile(item, filename) {
    method handlePaste (line 92) | handlePaste(e) {
    method isImage (line 104) | isImage(data) {
    method showUploading (line 115) | showUploading() {
    method appendImageFromUpload (line 128) | appendImageFromUpload(srcs) {
    method restoreUploaderStatus (line 138) | restoreUploaderStatus() {
    method insertString (line 146) | insertString(str) {
    method appendCodesFromHint (line 157) | appendCodesFromHint() {

FILE: assets/js/app/components/session.js
  class Session (line 1) | class Session {
    method refreshExcaptcha (line 2) | static refreshExcaptcha() {

FILE: assets/js/app/components/times.js
  class Times (line 115) | class Times {
    method humanize (line 116) | static humanize() {

FILE: assets/js/common/components/utils.js
  class Utils (line 1) | class Utils {
    method navActive (line 2) | static navActive() {

FILE: lib/mipha.ex
  class Mipha (line 1) | defmodule Mipha

FILE: lib/mipha/accounts/accounts.ex
  class Mipha.Accounts (line 1) | defmodule Mipha.Accounts
    method get_user! (line 29) | def get_user!(id), do: Repo.get!(User, id)
    method get_user (line 31) | def get_user(id), do: Repo.get(User, id)
    method create_user (line 45) | def create_user(attrs \\ %{}) do
    method update_user (line 63) | def update_user(%User{} = user, attrs) do
    method delete_user (line 81) | def delete_user(%User{} = user) do
    method change_user (line 94) | def change_user(%User{} = user) do
    method register_user (line 111) | def register_user(attrs) do
    method authenticate (line 130) | def authenticate(attrs) do
    method get_user_by (line 143) | def get_user_by(clauses, _opts \\ []) do
    method get_user_by_username (line 162) | def get_user_by_username(username, opts \\ []),
    method get_user_by_email (line 178) | def get_user_by_email(email, opts \\ []),
    method check_user_password (line 181) | defp check_user_password(user, password) do
    method get_user_count (line 198) | def get_user_count do
    method get_total_user_count (line 207) | def get_total_user_count do
    method update_user_password (line 216) | def update_user_password(user, attrs) do
    method mark_as_verified (line 231) | def mark_as_verified(user) do
    method update_reset_password (line 239) | def update_reset_password(user, attrs) do
    method login_or_register_from_github (line 248) | def login_or_register_from_github(%{nickname: nickname, name: nil, ema...
    method login_or_register_from_github (line 252) | def login_or_register_from_github(%{nickname: nickname, name: _name, e...
    method login_or_register_from_github (line 256) | def login_or_register_from_github(%{nickname: nickname, name: _name, e...
    method github_repositories (line 285) | def github_repositories(%User{} = user) do
    method github_repositories (line 292) | def github_repositories(%Team{} = team) do
    method fetch_github_repos (line 311) | defp fetch_github_repos(items, _) do
    method handle_response (line 316) | defp handle_response(%HTTPoison.Response{body: body, status_code: 200}...
    method handle_response (line 324) | defp handle_response(%HTTPoison.Response{body: _, status_code: 404}) do
    method github_repos_cache_key (line 329) | defp github_repos_cache_key(target) do
    method github_repos_url (line 334) | defp github_repos_url(target) do
    method github_handle (line 343) | def github_handle(%User{} = user) do
    method github_handle (line 347) | def github_handle(%Team{} = team) do
    method search_mention_user (line 355) | def search_mention_user(%User{} = user, q) do
    method user_register_changeset (line 372) | def user_register_changeset(attrs \\ %{}), do: User.register_changeset...
    method user_login_changeset (line 378) | def user_login_changeset(attrs \\ %{}), do: User.login_changeset(%User...
    method user_update_password_changeset (line 384) | def user_update_password_changeset(attrs \\ %{}),
    method change_user_reset_password (line 391) | def change_user_reset_password(%User{} = user, attrs \\ %{}),
    method list_locations (line 405) | def list_locations do
    method get_location! (line 423) | def get_location!(id) do
    method create_location (line 441) | def create_location(attrs \\ %{}) do
    method update_location (line 459) | def update_location(%Location{} = location, attrs) do
    method delete_location (line 477) | def delete_location(%Location{} = location) do
    method change_location (line 490) | def change_location(%Location{} = location) do
    method get_company! (line 510) | def get_company!(id), do: Repo.get!(Company, id)
    method create_company (line 524) | def create_company(attrs \\ %{}) do
    method update_company (line 542) | def update_company(%Company{} = company, attrs) do
    method delete_company (line 560) | def delete_company(%Company{} = company) do
    method change_company (line 573) | def change_company(%Company{} = company) do
    method list_teams (line 586) | def list_teams do
    method get_team! (line 604) | def get_team!(id) do
    method create_team (line 622) | def create_team(attrs \\ %{}) do
    method update_team (line 640) | def update_team(%Team{} = team, attrs) do
    method delete_team (line 658) | def delete_team(%Team{} = team) do
    method change_team (line 671) | def change_team(%Team{} = team) do
    method list_users_teams (line 686) | def list_users_teams do
    method get_user_team! (line 704) | def get_user_team!(id), do: Repo.get!(UserTeam, id)
    method create_user_team (line 718) | def create_user_team(attrs \\ %{}) do
    method update_user_team (line 736) | def update_user_team(%UserTeam{} = user_team, attrs) do
    method delete_user_team (line 754) | def delete_user_team(%UserTeam{} = user_team) do
    method change_user_team (line 767) | def change_user_team(%UserTeam{} = user_team) do

FILE: lib/mipha/accounts/company.ex
  class Mipha.Accounts.Company (line 1) | defmodule Mipha.Accounts.Company
    method changeset (line 22) | def changeset(company, attrs) do

FILE: lib/mipha/accounts/location.ex
  class Mipha.Accounts.Location (line 1) | defmodule Mipha.Accounts.Location
    method changeset (line 21) | def changeset(location, attrs) do

FILE: lib/mipha/accounts/queries.ex
  class Mipha.Accounts.Queries (line 1) | defmodule Mipha.Accounts.Queries
    method list_users (line 11) | def list_users, do: User |> order_by([q], asc: q.inserted_at)
    method list_teams (line 17) | def list_teams, do: Team
    method list_companies (line 23) | def list_companies, do: Company

FILE: lib/mipha/accounts/team.ex
  class Mipha.Accounts.Team (line 1) | defmodule Mipha.Accounts.Team
    method changeset (line 29) | def changeset(team, attrs) do

FILE: lib/mipha/accounts/user.ex
  class Mipha.Accounts.User (line 1) | defmodule Mipha.Accounts.User
    method changeset (line 59) | def changeset(user, attrs) do
    method login_changeset (line 92) | def login_changeset(user, attrs) do
    method register_changeset (line 106) | def register_changeset(user, attrs) do
    method update_password_changeset (line 142) | def update_password_changeset(user, attrs) do
    method reset_password_changeset (line 167) | def reset_password_changeset(user, attrs) do
    method validate_password_confirmation (line 181) | defp validate_password_confirmation(%{changes: changes} = changeset) do
    method put_pass_hash (line 187) | defp put_pass_hash(changeset) do

FILE: lib/mipha/accounts/user_team.ex
  class Mipha.Accounts.UserTeam (line 1) | defmodule Mipha.Accounts.UserTeam
    method changeset (line 19) | def changeset(user_team, attrs) do

FILE: lib/mipha/application.ex
  class Mipha.Application (line 1) | defmodule Mipha.Application
    method start (line 8) | def start(_type, _args) do
    method config_change (line 33) | def config_change(changed, _new, removed) do

FILE: lib/mipha/collections/collection.ex
  class Mipha.Collections.Collection (line 1) | defmodule Mipha.Collections.Collection
    method by_user (line 26) | def by_user(query \\ __MODULE__, %User{id: user_id}),
    method by_topic (line 33) | def by_topic(query \\ __MODULE__, %Topic{id: topic_id}),
    method changeset (line 37) | def changeset(collection, attrs) do

FILE: lib/mipha/collections/collections.ex
  class Mipha.Collections (line 1) | defmodule Mipha.Collections
    method list_collections (line 22) | def list_collections do
    method get_collection! (line 40) | def get_collection!(id), do: Repo.get!(Collection, id)
    method create_collection (line 54) | def create_collection(attrs \\ %{}) do
    method update_collection (line 72) | def update_collection(%Collection{} = collection, attrs) do
    method delete_collection (line 90) | def delete_collection(%Collection{} = collection) do
    method change_collection (line 110) | def change_collection(%Collection{} = collection) do
    method get_collection_count (line 124) | def get_collection_count(%User{} = user) do
    method has_collected? (line 159) | def has_collected?(clauses) do
    method insert_collection (line 179) | def insert_collection(attrs \\ %{}) do

FILE: lib/mipha/collections/queries.ex
  class Mipha.Collections.Queries (line 1) | defmodule Mipha.Collections.Queries
    method cond_collections (line 17) | def cond_collections(opts \\ []) do
    method filter_from_clauses (line 23) | defp filter_from_clauses(opts) do

FILE: lib/mipha/factory.ex
  class Mipha.Factory (line 1) | defmodule Mipha.Factory
    method user_factory (line 23) | def user_factory do
    method topic_factory (line 32) | def topic_factory do
    method node_factory (line 42) | def node_factory do
    method reply_factory (line 49) | def reply_factory do
    method collection_factory (line 58) | def collection_factory do
    method notification_factory (line 66) | def notification_factory do
    method user_notification_factory (line 74) | def user_notification_factory do
    method star_factory (line 82) | def star_factory do
    method follow_factory (line 89) | def follow_factory do
    method company_factory (line 97) | def company_factory do
    method team_factory (line 104) | def team_factory do

FILE: lib/mipha/follows/follow.ex
  class Mipha.Follows.Follow (line 1) | defmodule Mipha.Follows.Follow
    method by_follower (line 26) | def by_follower(query \\ __MODULE__, %User{id: follower_id}),
    method by_user (line 33) | def by_user(query \\ __MODULE__, %User{id: user_id}),
    method preload_user (line 40) | def preload_user(follow), do: Repo.preload(follow, [:user])
    method changeset (line 43) | def changeset(follow, attrs) do

FILE: lib/mipha/follows/follows.ex
  class Mipha.Follows (line 1) | defmodule Mipha.Follows
    method list_follows (line 28) | def list_follows do
    method get_follow! (line 46) | def get_follow!(id), do: Repo.get!(Follow, id)
    method create_follow (line 84) | def create_follow(attrs \\ %{}) do
    method update_follow (line 102) | def update_follow(%Follow{} = follow, attrs) do
    method delete_follow (line 121) | def delete_follow(%Follow{} = follow) do
    method change_follow (line 141) | def change_follow(%Follow{} = follow) do
    method unfollow_user (line 158) | def unfollow_user(follower: follower, user: user) do
    method follow_user (line 184) | def follow_user(follower: follower, user: user) do
    method can_follow? (line 203) | def can_follow?(clauses) do
    method has_followed? (line 224) | def has_followed?(clauses) do
    method insert_follow (line 245) | def insert_follow(attrs \\ %{}) do
    method notify_followee_of_follow (line 254) | defp notify_followee_of_follow(multi) do
    method get_follower_count (line 295) | def get_follower_count(clauses) do
    method get_followee_count (line 316) | def get_followee_count(%User{} = user) do

FILE: lib/mipha/follows/queries.ex
  class Mipha.Follows.Queries (line 1) | defmodule Mipha.Follows.Queries
    method cond_follows (line 24) | def cond_follows(opts \\ []) do
    method filter_from_clauses (line 30) | defp filter_from_clauses(opts) do

FILE: lib/mipha/mailer.ex
  class Mipha.Mailer (line 1) | defmodule Mipha.Mailer

FILE: lib/mipha/markdown.ex
  class Mipha.Markdown (line 1) | defmodule Mipha.Markdown
    method render (line 6) | def render(body) do
    method as_html! (line 11) | defp as_html!(body) do
    method example (line 17) | def example do
    method earmark_options (line 21) | def earmark_options do

FILE: lib/mipha/markdown/auto_linker.ex
  class Mipha.Markdown.AutoLinker (line 1) | defmodule Mipha.Markdown.AutoLinker
    method run (line 10) | def run(body) do

FILE: lib/mipha/markdown/embed_video_replacer.ex
  class Mipha.Markdown.EmbedVideoReplacer (line 1) | defmodule Mipha.Markdown.EmbedVideoReplacer
    method run (line 9) | def run(body) do
    method youku_replacer (line 24) | defp youku_replacer(whole_match) do
    method youtube_replacer (line 33) | defp youtube_replacer(whole_match) do
    method embed_tag (line 42) | defp embed_tag(src) do

FILE: lib/mipha/markdown/emoji_replacer.ex
  class Mipha.Markdown.EmojiReplacer (line 1) | defmodule Mipha.Markdown.EmojiReplacer
    method run (line 10) | def run(body) do
    method emojify_short_name (line 15) | defp emojify_short_name(whole_match, short_name) do

FILE: lib/mipha/markdown/html_renderer.ex
  class Mipha.Markdown.HtmlRenderer (line 9) | defmodule Mipha.Markdown.HtmlRenderer
    method render (line 23) | def render(blocks, %Context{options: %Options{mapper: mapper}} = conte...
    method render_block (line 39) | defp render_block(%Block.Para{lnb: lnb, lines: lines, attrs: attrs}, c...
    method render_block (line 52) | defp render_block(%Block.Html{html: html}, context) do
    method render_block (line 56) | defp render_block(%Block.HtmlOther{html: html}, context) do
    method render_block (line 61) | defp render_block(%Block.Ruler{lnb: lnb, type: "-", attrs: attrs}, con...
    method render_block (line 65) | defp render_block(%Block.Ruler{lnb: lnb, type: "_", attrs: attrs}, con...
    method render_block (line 69) | defp render_block(%Block.Ruler{lnb: lnb, type: "*", attrs: attrs}, con...
    method render_block (line 74) | defp render_block(
    method render_block (line 84) | defp render_block(%Block.BlockQuote{lnb: lnb, blocks: blocks, attrs: a...
    method render_block (line 91) | defp render_block(
    method render_block (line 114) | defp render_block(
    method render_block (line 128) | defp render_block(
    method render_block (line 150) | defp render_block(%Block.ListItem{lnb: lnb, blocks: blocks, attrs: att...
    method render_block (line 157) | defp render_block(%Block.FnList{blocks: footnotes}, context) do
    method render_block (line 168) | defp render_block(%Block.Ial{verbatim: verbatim}, context) do
    method render_block (line 173) | defp render_block(%Block.IdDef{}, context), do: {context, ""}
    method render_block (line 176) | defp render_block(%Block.Plugin{lines: lines, handler: handler}, conte...
    method br (line 185) | def br, do: "<br/>"
    method codespan (line 186) | def codespan(text), do: ~s[<code class="inline">#{text}</code>]
    method em (line 187) | def em(text), do: "<em>#{text}</em>"
    method strong (line 188) | def strong(text), do: "<strong>#{text}</strong>"
    method strikethrough (line 189) | def strikethrough(text), do: "<del>#{text}</del>"
    method link (line 191) | def link(url, text), do: ~s[<a href="#{url}">#{text}</a>]
    method link (line 192) | def link(url, text, nil), do: ~s[<a href="#{url}">#{text}</a>]
    method link (line 193) | def link(url, text, title), do: ~s[<a href="#{url}" title="#{title}">#...
    method image (line 195) | def image(path, alt, nil) do
    method image (line 199) | def image(path, alt, title) do
    method footnote_link (line 203) | def footnote_link(ref, backref, number),
    method add_trs (line 207) | def add_trs(context, rows, tag, aligns, lnb) do
    method add_tds (line 219) | defp add_tds(context, row, tag, aligns, lnb) do
    method add_td_fn (line 223) | defp add_td_fn(row, tag, aligns, lnb) do
    method append_footnote_link (line 238) | def append_footnote_link(%Block.FnDef{} = note) do
    method append_footnote_link (line 250) | def append_footnote_link(%Block.Para{lines: lines} = block, fnlink) do
    method append_footnote_link (line 256) | def append_footnote_link(block, fnlink) do
    method render_code (line 260) | def render_code(%Block.Code{lines: lines}) do
    method code_classes (line 264) | defp code_classes(language, prefix) do
    method transform_value (line 270) | defp transform_value(context, transformer) do

FILE: lib/mipha/markdown/mention_replacer.ex
  class Mipha.Markdown.MentionReplacer (line 1) | defmodule Mipha.Markdown.MentionReplacer
    method run (line 10) | def run(body) do
    method existed_user_replacer (line 15) | defp existed_user_replacer(whole_match, match_name) do

FILE: lib/mipha/notifications/notification.ex
  class Mipha.Notifications.Notification (line 1) | defmodule Mipha.Notifications.Notification
    method by_actor (line 57) | def by_actor(query \\ Notification, %User{id: actor_id}),
    method preload_actor (line 64) | def preload_actor(%Notification{} = notification), do: Repo.preload(no...
    method preload_topic (line 70) | def preload_topic(%Notification{} = notification), do: Repo.preload(no...
    method preload_reply (line 76) | def preload_reply(%Notification{} = notification), do: Repo.preload(no...
    method preload_user (line 82) | def preload_user(%Notification{} = notification), do: Repo.preload(not...
    method changeset (line 85) | def changeset(notification, attrs) do
    method maybe_put_notified_users (line 109) | defp maybe_put_notified_users(%Ecto.Changeset{} = changeset, attrs) do

FILE: lib/mipha/notifications/notifications.ex
  class Mipha.Notifications (line 1) | defmodule Mipha.Notifications
    method list_notifications (line 33) | def list_notifications do
    method get_notification! (line 51) | def get_notification!(id), do: Repo.get!(Notification, id)
    method create_notification (line 65) | def create_notification(attrs \\ %{}) do
    method update_notification (line 83) | def update_notification(%Notification{} = notification, attrs) do
    method delete_notification (line 101) | def delete_notification(%Notification{} = notification) do
    method change_notification (line 114) | def change_notification(%Notification{} = notification) do
    method read_notification (line 123) | def read_notification(%UserNotification{} = user_notification) do
    method mark_read_notification (line 135) | def mark_read_notification(user) do
    method clean_notification (line 146) | def clean_notification(user) do
    method actor (line 165) | def actor(%Notification{} = notification) do
    method object (line 207) | def object(_), do: nil
    method unread_notification_count (line 213) | def unread_notification_count(user) do
    method list_users_notifications (line 229) | def list_users_notifications do
    method get_user_notification! (line 247) | def get_user_notification!(id), do: Repo.get!(UserNotification, id)
    method create_user_notification (line 261) | def create_user_notification(attrs \\ %{}) do
    method insert_notification (line 271) | def insert_notification(attrs) do
    method insert_notification (line 278) | defp insert_notification(multi, attrs) do
    method update_user_notification (line 296) | def update_user_notification(%UserNotification{} = user_notification, ...
    method delete_user_notification (line 314) | def delete_user_notification(%UserNotification{} = user_notification) do
    method change_user_notification (line 327) | def change_user_notification(%UserNotification{} = user_notification) do

FILE: lib/mipha/notifications/queries.ex
  class Mipha.Notifications.Queries (line 1) | defmodule Mipha.Notifications.Queries
    method cond_user_notifications (line 13) | def cond_user_notifications(%User{} = user) do

FILE: lib/mipha/notifications/user_notification.ex
  class Mipha.Notifications.UserNotification (line 1) | defmodule Mipha.Notifications.UserNotification
    method by_user (line 26) | def by_user(query \\ UserNotification, %User{id: user_id}),
    method unread (line 33) | def unread(query \\ __MODULE__),
    method preload_user (line 40) | def preload_user(%UserNotification{} = user_notification),
    method update_changeset (line 47) | def update_changeset(%UserNotification{} = user_notification, attrs) do
    method changeset (line 59) | def changeset(user_notification, attrs) do

FILE: lib/mipha/qiniu.ex
  class Mipha.Qiniu (line 1) | defmodule Mipha.Qiniu
    method upload (line 7) | def upload(path) do
    method q_url (line 20) | def q_url(value) do
    method generate_key (line 24) | defp generate_key do

FILE: lib/mipha/regexp.ex
  class Mipha.Regexp (line 1) | defmodule Mipha.Regexp
    method username (line 6) | def username do
    method email (line 10) | def email do

FILE: lib/mipha/replies/queries.ex
  class Mipha.Replies.Queries (line 1) | defmodule Mipha.Replies.Queries
    method list_replies (line 11) | def list_replies, do: Reply
    method cond_replies (line 26) | def cond_replies(opts) do
    method filter_from_clauses (line 32) | defp filter_from_clauses(opts) do

FILE: lib/mipha/replies/replies.ex
  class Mipha.Replies (line 1) | defmodule Mipha.Replies
    method list_repies (line 33) | def list_repies do
    method get_reply! (line 51) | def get_reply!(id), do: Repo.get!(Reply, id)
    method create_reply (line 65) | def create_reply(attrs \\ %{}) do
    method update_reply (line 83) | def update_reply(%Reply{} = reply, attrs) do
    method delete_reply (line 101) | def delete_reply(%Reply{} = reply) do
    method decrease_topic_reply_count (line 108) | defp decrease_topic_reply_count(multi) do
    method change_reply (line 131) | def change_reply(%Reply{} = reply) do
    method get_reply_count (line 148) | def get_reply_count(clauses) do
    method get_total_reply_count (line 167) | def get_total_reply_count do
    method get_replyable_from_clauses (line 172) | defp get_replyable_from_clauses(clauses) do
    method recent_replies (line 184) | def recent_replies(%User{} = user) do
    method insert_reply (line 205) | def insert_reply(user, attrs \\ %{}) do
    method maybe_notify_mention_users_of_new_reply (line 220) | def maybe_notify_mention_users_of_new_reply(multi, attrs) do
    method update_related_topic (line 245) | defp update_related_topic(multi) do
    method notify_topic_owner_of_new_reply (line 268) | defp notify_topic_owner_of_new_reply(multi) do
    method maybe_notify_parent_reply_owner_of_new_reply (line 321) | defp maybe_notify_parent_reply_owner_of_new_reply(multi, _), do: multi
    method maybe_notify_follower_of_new_reply (line 324) | defp maybe_notify_follower_of_new_reply(multi) do
    method notifiable_users_of_reply (line 345) | defp notifiable_users_of_reply(%Reply{} = reply) do
    method author (line 365) | def author(%Reply{} = reply) do

FILE: lib/mipha/replies/reply.ex
  class Mipha.Replies.Reply (line 1) | defmodule Mipha.Replies.Reply
    method is_child (line 35) | def is_child(query \\ __MODULE__),
    method by_topic (line 42) | def by_topic(query \\ __MODULE__, %Topic{id: topic_id}),
    method by_parent (line 49) | def by_parent(query \\ __MODULE__, %__MODULE__{id: parent_id}),
    method by_user (line 56) | def by_user(query \\ __MODULE__, %User{id: user_id}),
    method recent (line 63) | def recent(query \\ __MODULE__),
    method preload_user (line 70) | def preload_user(reply), do: Repo.preload(reply, [:user])
    method preload_topic (line 76) | def preload_topic(reply), do: Repo.preload(reply, [:topic])
    method preload_parent (line 82) | def preload_parent(reply), do: Repo.preload(reply, [:parent])
    method changeset (line 85) | def changeset(reply, attrs) do

FILE: lib/mipha/repo.ex
  class Mipha.Repo (line 1) | defmodule Mipha.Repo
    method init (line 8) | def init(_, opts) do

FILE: lib/mipha/stars/star.ex
  class Mipha.Stars.Star (line 1) | defmodule Mipha.Stars.Star
    method by_topic (line 29) | def by_topic(query \\ __MODULE__, %Topic{id: topic_id}),
    method by_reply (line 36) | def by_reply(query \\ __MODULE__, %Reply{id: reply_id}),
    method preload_reply (line 43) | def preload_reply(star), do: Repo.preload(star, :reply)
    method preload_topic (line 49) | def preload_topic(star), do: Repo.preload(star, :topic)
    method changeset (line 52) | def changeset(star, attrs) do

FILE: lib/mipha/stars/stars.ex
  class Mipha.Stars (line 1) | defmodule Mipha.Stars
    method list_stars (line 34) | def list_stars do
    method get_star! (line 52) | def get_star!(id), do: Repo.get!(Star, id)
    method create_star (line 66) | def create_star(attrs \\ %{}) do
    method update_star (line 84) | def update_star(%Star{} = star, attrs) do
    method delete_star (line 103) | def delete_star(%Star{} = star) do
    method delete_star (line 108) | def delete_star(clauses) do
    method decrease_related_count (line 137) | defp decrease_related_count(multi, _), do: multi
    method change_star (line 148) | def change_star(%Star{} = star) do
    method insert_star (line 196) | def insert_star(attrs) do
    method increase_related_count (line 228) | defp increase_related_count(multi, _), do: multi
    method notify_author_of_starrable (line 230) | defp notify_author_of_starrable(multi) do
    method has_starred? (line 286) | def has_starred?(clauses) do
    method get_starrable_from_clauses (line 302) | defp get_starrable_from_clauses(clauses) do

FILE: lib/mipha/token.ex
  class Mipha.Token (line 1) | defmodule Mipha.Token
    method generate_token (line 8) | def generate_token(%User{id: user_id}) do
    method verify_token (line 12) | def verify_token(token) do

FILE: lib/mipha/topics/node.ex
  class Mipha.Topics.Node (line 1) | defmodule Mipha.Topics.Node
    method is_child (line 28) | def is_child(query \\ __MODULE__), do: from(q in query, where: not is_...
    method is_parent (line 34) | def is_parent(query \\ __MODULE__), do: from(q in query, where: is_nil...
    method preload_children (line 40) | def preload_children(node), do: Repo.preload(node, :children)
    method changeset (line 43) | def changeset(node, attrs) do

FILE: lib/mipha/topics/queries.ex
  class Mipha.Topics.Queries (line 1) | defmodule Mipha.Topics.Queries
    method list_topics (line 11) | def list_topics, do: Topic
    method list_nodes (line 17) | def list_nodes, do: Node
    method job_topics (line 23) | def job_topics do
    method cond_topics (line 47) | def cond_topics(opts) do
    method filter_from_clauses (line 54) | defp filter_from_clauses(opts), do: do_filter_from_clauses(opts)
    method do_filter_from_clauses (line 56) | defp do_filter_from_clauses(type: :educational), do: Topic.educational()
    method do_filter_from_clauses (line 57) | defp do_filter_from_clauses(type: :featured), do: Topic.featured()
    method do_filter_from_clauses (line 58) | defp do_filter_from_clauses(type: :no_reply), do: Topic.no_reply()
    method do_filter_from_clauses (line 59) | defp do_filter_from_clauses(type: :popular), do: Topic.popular()
    method do_filter_from_clauses (line 60) | defp do_filter_from_clauses(node: node), do: Topic.by_node(node)
    method do_filter_from_clauses (line 61) | defp do_filter_from_clauses(user: user), do: Topic.by_user(user)
    method do_filter_from_clauses (line 62) | defp do_filter_from_clauses(user_ids: user_ids), do: Topic.by_user_ids...
    method do_filter_from_clauses (line 63) | defp do_filter_from_clauses(_), do: Topic

FILE: lib/mipha/topics/topic.ex
  class Mipha.Topics.Topic (line 1) | defmodule Mipha.Topics.Topic
    method job (line 54) | def job(query \\ __MODULE__),
    method featured (line 61) | def featured(query \\ __MODULE__),
    method educational (line 68) | def educational(query \\ __MODULE__),
    method no_reply (line 75) | def no_reply(query \\ __MODULE__),
    method popular (line 82) | def popular(query \\ __MODULE__),
    method by_node (line 89) | def by_node(query \\ __MODULE__, %Node{id: node_id}),
    method by_user (line 96) | def by_user(query \\ __MODULE__, %User{id: user_id}),
    method by_user_ids (line 103) | def by_user_ids(query \\ __MODULE__, list),
    method recent (line 110) | def recent(query \\ __MODULE__),
    method base_order (line 117) | def base_order(query \\ __MODULE__),
    method preload_user (line 124) | def preload_user(topic), do: Repo.preload(topic, [:user, :last_reply_u...
    method preload_replies (line 130) | def preload_replies(topic), do: Repo.preload(topic, :replies)
    method preload_node (line 136) | def preload_node(topic), do: Repo.preload(topic, :node)
    method preload_all (line 142) | def preload_all(topic) do
    method counter (line 152) | def counter(%Topic{id: topic_id}, :inc, :visit_count) do
    method counter (line 158) | def counter(%Topic{id: topic_id}, :inc, :reply_count) do
    method counter (line 164) | def counter(%Topic{id: topic_id}, :dec, :reply_count) do
    method changeset (line 171) | def changeset(topic, attrs) do

FILE: lib/mipha/topics/topics.ex
  class Mipha.Topics (line 1) | defmodule Mipha.Topics
    method list_topics (line 30) | def list_topics do
    method get_topic! (line 49) | def get_topic!(id) do
    method create_topic (line 67) | def create_topic(attrs \\ %{}) do
    method update_topic (line 85) | def update_topic(%Topic{} = topic, attrs) do
    method delete_topic (line 103) | def delete_topic(%Topic{} = topic) do
    method change_topic (line 117) | def change_topic(%Topic{} = topic \\ %Topic{}), do: Topic.changeset(to...
    method topic_visit_counter (line 122) | def topic_visit_counter(%Topic{} = topic), do: Topic.counter(topic, :i...
    method insert_topic (line 137) | def insert_topic(user, attrs \\ %{}) do
    method maybe_notify_users_of_new_topic (line 148) | defp maybe_notify_users_of_new_topic(multi) do
    method maybe_notify_mention_users_of_new_topic (line 170) | def maybe_notify_mention_users_of_new_topic(multi, attrs) do
    method notifiable_users_of_topic (line 195) | def notifiable_users_of_topic(%Topic{} = topic) do
    method list_featured_topics (line 214) | def list_featured_topics do
    method get_topic_count (line 224) | def get_topic_count(%User{} = user) do
    method get_total_topic_count (line 234) | def get_total_topic_count do
    method recent_topics (line 243) | def recent_topics(%User{} = user) do
    method author (line 261) | def author(%Topic{} = topic) do
    method list_nodes (line 278) | def list_nodes do
    method get_node! (line 296) | def get_node!(id), do: Repo.get!(Node, id)
    method create_node (line 310) | def create_node(attrs \\ %{}) do
    method update_node (line 328) | def update_node(%Node{} = node, attrs) do
    method delete_node (line 346) | def delete_node(%Node{} = node) do
    method change_node (line 360) | def change_node(%Node{} = node \\ %Node{}), do: Node.changeset(node, %{})
    method list_parent_nodes (line 366) | def list_parent_nodes do

FILE: lib/mipha/utils/store.ex
  class Mipha.Utils.Store (line 1) | defmodule Mipha.Utils.Store
    method get (line 17) | def get(cache_key) do
    method get! (line 21) | def get!(cache_key) do
    method put (line 28) | def put(cache_key, cache_value) do
    method put! (line 32) | def put!(cache_key, cache_value) do

FILE: lib/mipha_web.ex
  class MiphaWeb (line 1) | defmodule MiphaWeb
    method controller (line 20) | def controller do
    method view (line 30) | def view do
    method router (line 52) | def router do
    method channel (line 60) | def channel do

FILE: lib/mipha_web/channels/presence.ex
  class MiphaWeb.Presence (line 1) | defmodule MiphaWeb.Presence

FILE: lib/mipha_web/channels/room_channel.ex
  class MiphaWeb.RoomChannel (line 1) | defmodule MiphaWeb.RoomChannel
    method join (line 12) | def join("room:lobby", payload, socket) do
    method handle_in (line 23) | def handle_in("ping", payload, socket) do
    method handle_in (line 29) | def handle_in("shout", payload, socket) do
    method handle_info (line 34) | def handle_info(:after_join, socket) do
    method authorized? (line 49) | defp authorized?(_payload) do

FILE: lib/mipha_web/channels/topic_channel.ex
  class MiphaWeb.TopicChannel (line 1) | defmodule MiphaWeb.TopicChannel
    method join (line 8) | def join("topic:" <> topic_id, payload, socket) do
    method handle_in (line 18) | def handle_in("ping", payload, socket) do
    method handle_in (line 24) | def handle_in("shout", payload, socket) do
    method authorized? (line 30) | defp authorized?(_payload) do

FILE: lib/mipha_web/channels/user_socket.ex
  class MiphaWeb.UserSocket (line 1) | defmodule MiphaWeb.UserSocket
    method connect (line 24) | def connect(%{"token" => token}, socket) do
    method connect (line 34) | def connect(_params, _socket), do: :error
    method id (line 46) | def id(_socket), do: nil

FILE: lib/mipha_web/controllers/admin/company_controller.ex
  class MiphaWeb.Admin.CompanyController (line 1) | defmodule MiphaWeb.Admin.CompanyController
    method index (line 7) | def index(conn, params) do
    method delete (line 12) | def delete(conn, %{"id" => id}) do

FILE: lib/mipha_web/controllers/admin/node_controller.ex
  class MiphaWeb.Admin.NodeController (line 1) | defmodule MiphaWeb.Admin.NodeController
    method index (line 7) | def index(conn, params) do
    method new (line 12) | def new(conn, _params) do
    method create (line 17) | def create(conn, %{"node" => node_params}) do
    method show (line 29) | def show(conn, %{"id" => id}) do
    method edit (line 34) | def edit(conn, %{"id" => id}) do
    method update (line 40) | def update(conn, %{"id" => id, "node" => node_params}) do
    method delete (line 54) | def delete(conn, %{"id" => id}) do

FILE: lib/mipha_web/controllers/admin/notification_controller.ex
  class MiphaWeb.Admin.NotificationController (line 1) | defmodule MiphaWeb.Admin.NotificationController
    method index (line 6) | def index(conn, _params) do
    method show (line 11) | def show(conn, %{"id" => id}) do
    method delete (line 16) | def delete(conn, %{"id" => id}) do

FILE: lib/mipha_web/controllers/admin/page_controller.ex
  class MiphaWeb.Admin.PageController (line 1) | defmodule MiphaWeb.Admin.PageController
    method index (line 4) | def index(conn, _) do

FILE: lib/mipha_web/controllers/admin/reply_controller.ex
  class MiphaWeb.Admin.ReplyController (line 1) | defmodule MiphaWeb.Admin.ReplyController
    method index (line 7) | def index(conn, params) do
    method show (line 12) | def show(conn, %{"id" => id}) do
    method delete (line 17) | def delete(conn, %{"id" => id}) do

FILE: lib/mipha_web/controllers/admin/team_controller.ex
  class MiphaWeb.Admin.TeamController (line 1) | defmodule MiphaWeb.Admin.TeamController
    method index (line 7) | def index(conn, params) do
    method delete (line 12) | def delete(conn, %{"id" => id}) do

FILE: lib/mipha_web/controllers/admin/topic_controller.ex
  class MiphaWeb.Admin.TopicController (line 1) | defmodule MiphaWeb.Admin.TopicController
    method index (line 6) | def index(conn, params) do

FILE: lib/mipha_web/controllers/admin/user_controller.ex
  class MiphaWeb.Admin.UserController (line 1) | defmodule MiphaWeb.Admin.UserController
    method index (line 7) | def index(conn, params) do
    method delete (line 12) | def delete(conn, %{"id" => id}) do

FILE: lib/mipha_web/controllers/auth_controller.ex
  class MiphaWeb.AuthController (line 1) | defmodule MiphaWeb.AuthController
    method login (line 8) | def login(conn, _params) do
    method delete (line 13) | def delete(conn, _params) do
    method request (line 22) | def request(conn, %{"provider" => "identity"}) do
    method callback (line 27) | def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
    method callback (line 33) | def callback(%{assigns: %{ueberauth_auth: auth}} = conn, params) do
    method ok_login (line 78) | defp ok_login(conn, user) do
    method authorized_user (line 86) | defp authorized_user(conn, _) do

FILE: lib/mipha_web/controllers/callback_controller.ex
  class MiphaWeb.CallbackController (line 1) | defmodule MiphaWeb.CallbackController
    method qiniu (line 9) | def qiniu(conn, %{"file" => file_params}) do

FILE: lib/mipha_web/controllers/company_controller.ex
  class MiphaWeb.CompanyController (line 1) | defmodule MiphaWeb.CompanyController
    method action (line 4) | def action(conn, _) do

FILE: lib/mipha_web/controllers/location_controller.ex
  class MiphaWeb.LocationController (line 1) | defmodule MiphaWeb.LocationController
    method index (line 6) | def index(conn, _) do
    method show (line 10) | def show(conn, %{"id" => id}) do

FILE: lib/mipha_web/controllers/notification_controller.ex
  class MiphaWeb.NotificationController (line 1) | defmodule MiphaWeb.NotificationController
    method index (line 8) | def index(conn, params) do
    method make_read (line 21) | def make_read(conn, _params) do
    method clean (line 31) | def clean(conn, _params) do

FILE: lib/mipha_web/controllers/page_controller.ex
  class MiphaWeb.PageController (line 1) | defmodule MiphaWeb.PageController
    method index (line 6) | def index(conn, _params) do
    method markdown (line 10) | def markdown(conn, _) do

FILE: lib/mipha_web/controllers/reply_controller.ex
  class MiphaWeb.ReplyController (line 1) | defmodule MiphaWeb.ReplyController
    method action (line 9) | def action(conn, _) do
    method create (line 14) | def create(conn, %{"reply" => reply_params}, topic) do
    method edit (line 31) | def edit(conn, %{"id" => id}, topic) do
    method update (line 43) | def update(conn, %{"id" => id, "reply" => reply_params}, topic) do
    method delete (line 59) | def delete(conn, %{"id" => id}, topic) do
    method star (line 68) | def star(conn, %{"reply_id" => reply_id}, topic) do
    method unstar (line 89) | def unstar(conn, %{"reply_id" => reply_id}, topic) do

FILE: lib/mipha_web/controllers/search_controller.ex
  class MiphaWeb.SearchController (line 1) | defmodule MiphaWeb.SearchController
    method index (line 6) | def index(conn, _) do
    method users (line 13) | def users(conn, params) do

FILE: lib/mipha_web/controllers/session_controller.ex
  class MiphaWeb.SessionController (line 1) | defmodule MiphaWeb.SessionController
    method new (line 9) | def new(conn, _params) do
    method excaptcha (line 14) | def excaptcha(conn, _) do
    method create (line 25) | def create(conn, %{"user" => user_params, "_excaptcha" => captcha}) do
    method sent_welcome_email (line 54) | defp sent_welcome_email(user) do
    method ok_login (line 61) | defp ok_login(conn, user) do
    method authorized_user (line 69) | defp authorized_user(conn, _) do

FILE: lib/mipha_web/controllers/setting_controller.ex
  class MiphaWeb.SettingController (line 1) | defmodule MiphaWeb.SettingController
    method action (line 10) | def action(conn, _) do
    method update (line 19) | def update(conn, %{"user" => user_params}) do
    method update_account (line 28) | defp update_account(conn, user_params) do
    method update_profile (line 48) | defp update_profile(conn, user_params) do
    method update_password (line 62) | defp update_password(conn, user_params) do
    method update_reward (line 84) | defp update_reward(conn, user_params) do
    method build_attrs (line 107) | defp build_attrs({nil, params}, _), do: params
    method build_attrs (line 109) | defp build_attrs({%Plug.Upload{} = avatar, params}, field) do

FILE: lib/mipha_web/controllers/team_controller.ex
  class MiphaWeb.TeamController (line 1) | defmodule MiphaWeb.TeamController
    method index (line 7) | def index(conn, _params) do
    method show (line 11) | def show(conn, %{"id" => id} = params) do
    method people (line 27) | def people(conn, %{"id" => id}) do

FILE: lib/mipha_web/controllers/topic_controller.ex
  class MiphaWeb.TopicController (line 1) | defmodule MiphaWeb.TopicController
    method action (line 15) | def action(conn, _) do
    method do_fragment (line 44) | defp do_fragment(:suggest, conn) do
    method do_fragment (line 52) | defp do_fragment(:unsuggest, conn) do
    method do_fragment (line 60) | defp do_fragment(:close, conn) do
    method do_fragment (line 68) | defp do_fragment(:open, conn) do
    method do_fragment (line 76) | defp do_fragment(:excellent, conn) do
    method do_fragment (line 84) | defp do_fragment(:normal, conn) do
    method do_fragment (line 92) | defp do_fragment(_, conn) do
    method do_update (line 97) | defp do_update(conn, topic, attrs, flash) do
    method jobs (line 105) | def jobs(conn, params) do
    method new (line 116) | def new(conn, _params) do
    method edit (line 126) | def edit(conn, %{"id" => id}) do
    method create (line 138) | def create(conn, %{"topic" => topic_params}) do
    method update (line 154) | def update(conn, %{"id" => id, "topic" => topic_params}) do
    method show (line 169) | def show(conn, %{"id" => id}) do
    method delete (line 185) | def delete(conn, %{"id" => id}) do
    method preview (line 194) | def preview(conn, %{"body" => body}) do
    method star (line 198) | def star(conn, %{"id" => id}) do
    method unstar (line 219) | def unstar(conn, %{"id" => id}) do
    method collection (line 240) | def collection(conn, %{"id" => id}) do
    method uncollection (line 261) | def uncollection(conn, %{"id" => id}) do

FILE: lib/mipha_web/controllers/user_controller.ex
  class MiphaWeb.UserController (line 1) | defmodule MiphaWeb.UserController
    method action (line 9) | def action(conn, _) do
    method index (line 25) | def index(conn, params) do
    method show (line 35) | def show(conn, _params, user) do
    method topics (line 46) | def topics(conn, params, user) do
    method replies (line 56) | def replies(conn, params, user) do
    method following (line 66) | def following(conn, params, user) do
    method followers (line 76) | def followers(conn, params, user) do
    method collections (line 86) | def collections(conn, params, user) do
    method follow (line 96) | def follow(conn, _params, user) do
    method unfollow (line 115) | def unfollow(conn, _params, user) do
    method sent_forgot_password_email (line 134) | def sent_forgot_password_email(conn, %{"user" => user_params}) do
    method forgot_password (line 154) | def forgot_password(conn, _) do
    method reset_password (line 158) | def reset_password(conn, %{"token" => token}) do
    method reset_password (line 173) | def reset_password(conn, _) do
    method update_password (line 179) | def update_password(conn, %{"user" => user_params}) do
    method parse (line 219) | defp parse(nil), do: {:error, "nil"}
    method parse (line 220) | defp parse(valid), do: {:ok, valid}

FILE: lib/mipha_web/email.ex
  class MiphaWeb.Email (line 1) | defmodule MiphaWeb.Email
    method forgot_password (line 13) | def forgot_password(token, user) do
    method welcome (line 28) | def welcome(token, user) do
    method verify_email (line 43) | def verify_email(token, user) do
    method normal_email (line 55) | defp normal_email do

FILE: lib/mipha_web/endpoint.ex
  class MiphaWeb.Endpoint (line 1) | defmodule MiphaWeb.Endpoint
    method init (line 54) | def init(_key, config) do

FILE: lib/mipha_web/gettext.ex
  class MiphaWeb.Gettext (line 1) | defmodule MiphaWeb.Gettext

FILE: lib/mipha_web/plugs/attack.ex
  class MiphaWeb.Plug.Attack (line 1) | defmodule MiphaWeb.Plug.Attack
    method allow_action (line 39) | def allow_action(conn, {:throttle, data}, opts) do
    method allow_action (line 45) | def allow_action(conn, _data, _opts) do
    method block_action (line 49) | def block_action(conn, {:throttle, data}, opts) do
    method block_action (line 55) | def block_action(conn, _data, _opts) do
    method add_throttling_headers (line 61) | defp add_throttling_headers(conn, data) do

FILE: lib/mipha_web/plugs/current_user.ex
  class MiphaWeb.Plug.CurrentUser (line 1) | defmodule MiphaWeb.Plug.CurrentUser
    method init (line 9) | def init(opts), do: opts
    method call (line 11) | def call(conn, _opts) do

FILE: lib/mipha_web/plugs/locale.ex
  class MiphaWeb.Plug.Locale (line 1) | defmodule MiphaWeb.Plug.Locale
    method init (line 8) | def init(_opts), do: nil
    method call (line 10) | def call(conn, _opts) do

FILE: lib/mipha_web/plugs/require_admin.ex
  class MiphaWeb.Plug.RequireAdmin (line 1) | defmodule MiphaWeb.Plug.RequireAdmin
    method init (line 9) | def init(opts), do: opts
    method call (line 11) | def call(conn, _opts) do

FILE: lib/mipha_web/plugs/require_user.ex
  class MiphaWeb.Plug.RequireUser (line 1) | defmodule MiphaWeb.Plug.RequireUser
    method init (line 11) | def init(opts), do: opts
    method call (line 13) | def call(conn, _opts) do

FILE: lib/mipha_web/router.ex
  class MiphaWeb.Router (line 1) | defmodule MiphaWeb.Router

FILE: lib/mipha_web/session.ex
  class MiphaWeb.Session (line 1) | defmodule MiphaWeb.Session
    method current_user (line 9) | def current_user(%{assigns: %{current_user: u}}), do: u
    method current_user (line 11) | def current_user(conn) do
    method user_logged_in? (line 18) | def user_logged_in?(conn), do: current_user(conn)
    method user_token (line 20) | def user_token(conn) do
    method admin? (line 27) | def admin?(conn) do
    method get_current_user (line 34) | defp get_current_user(conn) do
    method get_session_from_cookies (line 45) | defp get_session_from_cookies do

FILE: lib/mipha_web/views/admin/company_view.ex
  class MiphaWeb.Admin.CompanyView (line 1) | defmodule MiphaWeb.Admin.CompanyView

FILE: lib/mipha_web/views/admin/node_view.ex
  class MiphaWeb.Admin.NodeView (line 1) | defmodule MiphaWeb.Admin.NodeView

FILE: lib/mipha_web/views/admin/notification_view.ex
  class MiphaWeb.Admin.NotificationView (line 1) | defmodule MiphaWeb.Admin.NotificationView

FILE: lib/mipha_web/views/admin/page_view.ex
  class MiphaWeb.Admin.PageView (line 1) | defmodule MiphaWeb.Admin.PageView

FILE: lib/mipha_web/views/admin/reply_view.ex
  class MiphaWeb.Admin.ReplyView (line 1) | defmodule MiphaWeb.Admin.ReplyView

FILE: lib/mipha_web/views/admin/team_view.ex
  class MiphaWeb.Admin.TeamView (line 1) | defmodule MiphaWeb.Admin.TeamView

FILE: lib/mipha_web/views/admin/topic_view.ex
  class MiphaWeb.Admin.TopicView (line 1) | defmodule MiphaWeb.Admin.TopicView

FILE: lib/mipha_web/views/admin/user_view.ex
  class MiphaWeb.Admin.UserView (line 1) | defmodule MiphaWeb.Admin.UserView

FILE: lib/mipha_web/views/auth_view.ex
  class MiphaWeb.AuthView (line 1) | defmodule MiphaWeb.AuthView

FILE: lib/mipha_web/views/company_view.ex
  class MiphaWeb.CompanyView (line 1) | defmodule MiphaWeb.CompanyView

FILE: lib/mipha_web/views/email_view.ex
  class MiphaWeb.EmailView (line 1) | defmodule MiphaWeb.EmailView

FILE: lib/mipha_web/views/error_helpers.ex
  class MiphaWeb.ErrorHelpers (line 1) | defmodule MiphaWeb.ErrorHelpers
    method error_tag (line 11) | def error_tag(form, field) do
    method translate_error (line 20) | def translate_error({msg, opts}) do

FILE: lib/mipha_web/views/error_view.ex
  class MiphaWeb.ErrorView (line 1) | defmodule MiphaWeb.ErrorView
    method template_not_found (line 13) | def template_not_found(template, _assigns) do

FILE: lib/mipha_web/views/layout_view.ex
  class MiphaWeb.LayoutView (line 1) | defmodule MiphaWeb.LayoutView
    method has_unread_notification? (line 9) | def has_unread_notification?(user) do
    method unread_notification_count (line 16) | def unread_notification_count(user) do

FILE: lib/mipha_web/views/location_view.ex
  class MiphaWeb.LocationView (line 1) | defmodule MiphaWeb.LocationView

FILE: lib/mipha_web/views/notification_view.ex
  class MiphaWeb.NotificationView (line 1) | defmodule MiphaWeb.NotificationView
    method group_by (line 6) | def group_by(notifications) do
    method transfer_action (line 11) | def transfer_action(notification) do
    method hour_format (line 21) | def hour_format(notification) do

FILE: lib/mipha_web/views/page_view.ex
  class MiphaWeb.PageView (line 1) | defmodule MiphaWeb.PageView

FILE: lib/mipha_web/views/reply_view.ex
  class MiphaWeb.ReplyView (line 1) | defmodule MiphaWeb.ReplyView

FILE: lib/mipha_web/views/search_view.ex
  class MiphaWeb.SearchView (line 1) | defmodule MiphaWeb.SearchView

FILE: lib/mipha_web/views/session_view.ex
  class MiphaWeb.SessionView (line 1) | defmodule MiphaWeb.SessionView

FILE: lib/mipha_web/views/setting_view.ex
  class MiphaWeb.SettingView (line 1) | defmodule MiphaWeb.SettingView
    method location_option (line 9) | def location_option do

FILE: lib/mipha_web/views/shared_view.ex
  class MiphaWeb.SharedView (line 1) | defmodule MiphaWeb.SharedView

FILE: lib/mipha_web/views/tag_helpers.ex
  class MiphaWeb.TagHelpers (line 1) | defmodule MiphaWeb.TagHelpers
    method time_tag (line 13) | def time_tag(time) do
    method user_level_tag (line 26) | def user_level_tag(user) do
    method cycle_tag (line 35) | def cycle_tag(number) do
    method topic_featured_tag (line 42) | def topic_featured_tag(topic) do
    method qn_url (line 48) | def qn_url(string) do
    method topic_title_tag (line 52) | def topic_title_tag(topic, reply \\ nil) do
    method user_name_tag (line 59) | def user_name_tag(user) do

FILE: lib/mipha_web/views/team_view.ex
  class MiphaWeb.TeamView (line 1) | defmodule MiphaWeb.TeamView
    method is_owner? (line 4) | def is_owner?(team, user) do

FILE: lib/mipha_web/views/topic_view.ex
  class MiphaWeb.TopicView (line 1) | defmodule MiphaWeb.TopicView
    method has_starred? (line 15) | def has_starred?(clauses) do
    method has_collected? (line 22) | def has_collected?(current_user, topic) do
    method get_topic_count (line 31) | def get_topic_count do
    method get_reply_count (line 38) | def get_reply_count do
    method get_user_count (line 45) | def get_user_count do
    method popular (line 52) | def popular(reply) do

FILE: lib/mipha_web/views/user_view.ex
  class MiphaWeb.UserView (line 1) | defmodule MiphaWeb.UserView
    method format_inserted_date (line 12) | def format_inserted_date(datetime) do
    method user_topics_count (line 16) | def user_topics_count(user) do
    method user_replies_count (line 20) | def user_replies_count(user) do
    method user_followers_count (line 24) | def user_followers_count(user) do
    method user_following_count (line 28) | def user_following_count(user) do
    method user_collections_count (line 32) | def user_collections_count(user) do
    method has_followed? (line 39) | def has_followed?(current_user, user) do
    method github_repos (line 43) | def github_repos(target) do
    method github_account (line 47) | def github_account(target) do

FILE: lib/mipha_web/views/view_helpers.ex
  class MiphaWeb.ViewHelpers (line 1) | defmodule MiphaWeb.ViewHelpers
    method markdown (line 8) | def markdown(body) do

FILE: mix.exs
  class Mipha.Mixfile (line 1) | defmodule Mipha.Mixfile
    method project (line 4) | def project do
    method application (line 27) | def application do
    method elixirc_paths (line 35) | defp elixirc_paths(:test), do: ["lib", "test/support"]
    method elixirc_paths (line 36) | defp elixirc_paths(_), do: ["lib"]
    method deps (line 41) | defp deps do
    method aliases (line 89) | defp aliases do

FILE: priv/repo/migrations/20180629095811_create_users.exs
  class Mipha.Repo.Migrations.CreateUsers (line 1) | defmodule Mipha.Repo.Migrations.CreateUsers
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180702075249_create_topics.exs
  class Mipha.Repo.Migrations.CreateTopics (line 1) | defmodule Mipha.Repo.Migrations.CreateTopics
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180702081217_create_replies.exs
  class Mipha.Repo.Migrations.CreateReplies (line 1) | defmodule Mipha.Repo.Migrations.CreateReplies
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180702081845_create_nodes.exs
  class Mipha.Repo.Migrations.CreateNodes (line 1) | defmodule Mipha.Repo.Migrations.CreateNodes
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180703094024_create_locations.exs
  class Mipha.Repo.Migrations.CreateLocations (line 1) | defmodule Mipha.Repo.Migrations.CreateLocations
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180704024042_create_collections.exs
  class Mipha.Repo.Migrations.CreateCollections (line 1) | defmodule Mipha.Repo.Migrations.CreateCollections
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180704024600_create_follows.exs
  class Mipha.Repo.Migrations.CreateFollows (line 1) | defmodule Mipha.Repo.Migrations.CreateFollows
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180704025308_create_stars.exs
  class Mipha.Repo.Migrations.CreateStars (line 1) | defmodule Mipha.Repo.Migrations.CreateStars
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180704032212_create_companies.exs
  class Mipha.Repo.Migrations.CreateCompanies (line 1) | defmodule Mipha.Repo.Migrations.CreateCompanies
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180704053120_create_teams.exs
  class Mipha.Repo.Migrations.CreateTeams (line 1) | defmodule Mipha.Repo.Migrations.CreateTeams
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180704053159_create_users_teams.exs
  class Mipha.Repo.Migrations.CreateUsersTeams (line 1) | defmodule Mipha.Repo.Migrations.CreateUsersTeams
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180712015550_create_notifications.exs
  class Mipha.Repo.Migrations.CreateNotifications (line 1) | defmodule Mipha.Repo.Migrations.CreateNotifications
    method change (line 4) | def change do

FILE: priv/repo/migrations/20180712015614_create_users_notifications.exs
  class Mipha.Repo.Migrations.CreateUsersNotifications (line 1) | defmodule Mipha.Repo.Migrations.CreateUsersNotifications
    method change (line 4) | def change do

FILE: test/mipha/accounts/accounts_test.exs
  class Mipha.AccountsTest (line 1) | defmodule Mipha.AccountsTest

FILE: test/mipha/accounts/company_test.exs
  class Mipha.Accounts.AccountTest (line 1) | defmodule Mipha.Accounts.AccountTest

FILE: test/mipha/accounts/location_test.exs
  class Mipha.Accounts.LocationTest (line 1) | defmodule Mipha.Accounts.LocationTest

FILE: test/mipha/accounts/queries_test.exs
  class Mipha.Accounts.QueriesTest (line 1) | defmodule Mipha.Accounts.QueriesTest

FILE: test/mipha/accounts/user_test.exs
  class Mipha.Accounts.UserTest (line 1) | defmodule Mipha.Accounts.UserTest

FILE: test/mipha/collections/collection_test.exs
  class Mipha.Collections.CollectionTest (line 1) | defmodule Mipha.Collections.CollectionTest

FILE: test/mipha/collections/collections_test.exs
  class Mipha.CollectionsTest (line 1) | defmodule Mipha.CollectionsTest

FILE: test/mipha/collections/queries_test.exs
  class Mipha.Collections.QueriesTest (line 1) | defmodule Mipha.Collections.QueriesTest

FILE: test/mipha/follows/follow_test.exs
  class Mipha.Follows.FollowTest (line 1) | defmodule Mipha.Follows.FollowTest

FILE: test/mipha/follows/follows_test.exs
  class Mipha.FollowsTest (line 1) | defmodule Mipha.FollowsTest

FILE: test/mipha/follows/queries_test.exs
  class Mipha.Follows.QueriesTest (line 1) | defmodule Mipha.Follows.QueriesTest

FILE: test/mipha/notifications/notification_test.exs
  class Mipha.Notifications.NotificationTest (line 1) | defmodule Mipha.Notifications.NotificationTest

FILE: test/mipha/notifications/notifications_test.exs
  class Mipha.NotificationsTest (line 1) | defmodule Mipha.NotificationsTest

FILE: test/mipha/notifications/queries_test.exs
  class Mipha.Notifications.QueriesTest (line 1) | defmodule Mipha.Notifications.QueriesTest

FILE: test/mipha/notifications/user_notification_test.exs
  class Mipha.Notifications.UserNotificationTest (line 1) | defmodule Mipha.Notifications.UserNotificationTest

FILE: test/mipha/replies/queries_test.exs
  class Mipha.Replies.QueriesTest (line 1) | defmodule Mipha.Replies.QueriesTest

FILE: test/mipha/replies/replies_test.exs
  class Mipha.RepliesTest (line 1) | defmodule Mipha.RepliesTest

FILE: test/mipha/replies/reply_test.exs
  class Mipha.Replies.ReplyTest (line 1) | defmodule Mipha.Replies.ReplyTest

FILE: test/mipha/stars/star_test.exs
  class Mipha.Stars.StarTest (line 1) | defmodule Mipha.Stars.StarTest

FILE: test/mipha/stars/stars_test.exs
  class Mipha.StarsTest (line 1) | defmodule Mipha.StarsTest

FILE: test/mipha/topics/node_test.exs
  class Mipha.Topics.NodeTest (line 1) | defmodule Mipha.Topics.NodeTest

FILE: test/mipha/topics/queries_test.exs
  class Mipha.Topics.QueriesTest (line 1) | defmodule Mipha.Topics.QueriesTest

FILE: test/mipha/topics/topic_test.exs
  class Mipha.Topics.TopicTest (line 1) | defmodule Mipha.Topics.TopicTest

FILE: test/mipha/topics/topics_test.exs
  class Mipha.TopicsTest (line 1) | defmodule Mipha.TopicsTest

FILE: test/mipha_web/channels/room_channel_test.exs
  class MiphaWeb.RoomChannelTest (line 1) | defmodule MiphaWeb.RoomChannelTest

FILE: test/mipha_web/channels/topic_channel_test.exs
  class MiphaWeb.TopicChannelTest (line 1) | defmodule MiphaWeb.TopicChannelTest

FILE: test/mipha_web/controllers/admin/company_controller_test.exs
  class MiphaWeb.Admin.CompanyControllerTest (line 1) | defmodule MiphaWeb.Admin.CompanyControllerTest

FILE: test/mipha_web/controllers/admin/node_controller_test.exs
  class MiphaWeb.Admin.NodeControllerTest (line 1) | defmodule MiphaWeb.Admin.NodeControllerTest

FILE: test/mipha_web/controllers/admin/page_controller_test.exs
  class MiphaWeb.Admin.PageControllerTest (line 1) | defmodule MiphaWeb.Admin.PageControllerTest

FILE: test/mipha_web/controllers/admin/reply_controller_test.exs
  class MiphaWeb.Admin.ReplyControllerTest (line 1) | defmodule MiphaWeb.Admin.ReplyControllerTest

FILE: test/mipha_web/controllers/admin/team_controller_test.exs
  class MiphaWeb.Admin.TeamControllerTest (line 1) | defmodule MiphaWeb.Admin.TeamControllerTest

FILE: test/mipha_web/controllers/admin/topic_controller_test.exs
  class MiphaWeb.Admin.TopicControllerTest (line 1) | defmodule MiphaWeb.Admin.TopicControllerTest

FILE: test/mipha_web/controllers/admin/user_controller_test.exs
  class MiphaWeb.Admin.UserControllerTest (line 1) | defmodule MiphaWeb.Admin.UserControllerTest

FILE: test/mipha_web/controllers/auth_controller_test.exs
  class MiphaWeb.AuthControllerTest (line 1) | defmodule MiphaWeb.AuthControllerTest

FILE: test/mipha_web/controllers/location_controller_test.exs
  class MiphaWeb.LocationControllerTest (line 1) | defmodule MiphaWeb.LocationControllerTest

FILE: test/mipha_web/controllers/notification_controller_test.exs
  class MiphaWeb.NotificationControllerTest (line 1) | defmodule MiphaWeb.NotificationControllerTest

FILE: test/mipha_web/controllers/page_controller_test.exs
  class MiphaWeb.PageControllerTest (line 1) | defmodule MiphaWeb.PageControllerTest

FILE: test/mipha_web/controllers/reply_controller_test.exs
  class MiphaWeb.ReplyControllerTest (line 1) | defmodule MiphaWeb.ReplyControllerTest

FILE: test/mipha_web/controllers/session_controller_test.exs
  class MiphaWeb.SessionControllerTest (line 1) | defmodule MiphaWeb.SessionControllerTest

FILE: test/mipha_web/controllers/setting_controller_test.exs
  class MiphaWeb.SettingControllerTest (line 1) | defmodule MiphaWeb.SettingControllerTest

FILE: test/mipha_web/controllers/topic_controller_test.exs
  class MiphaWeb.TopicControllerTest (line 1) | defmodule MiphaWeb.TopicControllerTest

FILE: test/mipha_web/controllers/user_controller_test.exs
  class MiphaWeb.UserControllerTest (line 1) | defmodule MiphaWeb.UserControllerTest

FILE: test/mipha_web/views/error_view_test.exs
  class MiphaWeb.ErrorViewTest (line 1) | defmodule MiphaWeb.ErrorViewTest

FILE: test/mipha_web/views/layout_view_test.exs
  class MiphaWeb.LayoutViewTest (line 1) | defmodule MiphaWeb.LayoutViewTest

FILE: test/mipha_web/views/page_view_test.exs
  class MiphaWeb.PageViewTest (line 1) | defmodule MiphaWeb.PageViewTest

FILE: test/support/channel_case.ex
  class MiphaWeb.ChannelCase (line 1) | defmodule MiphaWeb.ChannelCase

FILE: test/support/conn_case.ex
  class MiphaWeb.ConnCase (line 1) | defmodule MiphaWeb.ConnCase

FILE: test/support/data_case.ex
  class Mipha.DataCase (line 1) | defmodule Mipha.DataCase
    method errors_on (line 46) | def errors_on(changeset) do
Condensed preview — 319 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (518K chars).
[
  {
    "path": ".coveralls.yml",
    "chars": 70,
    "preview": "service_name: travis-pro\nrepo_token: 2ZS2MYr7uA24NM8smvOIExISDZpNgS5VN"
  },
  {
    "path": ".credo.exs",
    "chars": 6065,
    "preview": "# This file contains the configuration for Credo and you are probably reading\n# this after creating it with `mix credo.g"
  },
  {
    "path": ".formatter.exs",
    "chars": 168,
    "preview": "[\n  inputs: [\n    \"{lib,test,priv}/**/*.{ex,exs}\",\n    \"mix.exs\",\n    \".formatter.exs\",\n    \".iex.exs\",\n    \".credo.exs\""
  },
  {
    "path": ".github/workflows/elixir.yml",
    "chars": 801,
    "preview": "name: Elixir CI\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n\n    runs"
  },
  {
    "path": ".gitignore",
    "chars": 742,
    "preview": "# App artifacts\n/_build\n/db\n/deps\n/*.ez\n\n# Generated on crash by the VM\nerl_crash.dump\n\n# Generated on crash by NPM\nnpm-"
  },
  {
    "path": ".iex.exs",
    "chars": 413,
    "preview": "import Ecto.{Changeset, Query}\n\nalias Mipha.{\n  Repo,\n  Accounts,\n  Topics,\n  Replies,\n  Stars,\n  Collections,\n  Follows"
  },
  {
    "path": ".travis.yml",
    "chars": 259,
    "preview": "language: elixir\nelixir:\n  - '1.8.1'\naddons:\n  postgresql: '9.4'\nservices:\n  - postgresql\ncache:\n  directories:\n    - _b"
  },
  {
    "path": "LICENSE",
    "chars": 1076,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2018 Zven Wang\n\nPermission is hereby granted, free of charge, to any person obtaini"
  },
  {
    "path": "Makefile",
    "chars": 267,
    "preview": "# code helper\niex:\n\tiex --erl \"-kernel shell_history enabled\" -S mix\nserve:\n\tiex --erl \"-kernel shell_history enabled\" -"
  },
  {
    "path": "Procfile",
    "chars": 33,
    "preview": "web: MIX_ENV=prod mix phx.server\n"
  },
  {
    "path": "README.ZH.md",
    "chars": 2630,
    "preview": "# Mipha\n\n[![Build Status](https://travis-ci.org/zven21/mipha.svg?branch=master)](https://travis-ci.org/zven21/mipha)\n[!["
  },
  {
    "path": "README.md",
    "chars": 2726,
    "preview": "# Mipha\n\n[![Build Status](https://travis-ci.org/zven21/mipha.svg?branch=master)](https://travis-ci.org/zven21/mipha)\n[!["
  },
  {
    "path": "assets/.eslintrc.json",
    "chars": 92,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": [\"prettier\"],\n  \"rules\": {\n    \"indent\": 0\n  }\n}\n"
  },
  {
    "path": "assets/.yarnrc",
    "chars": 21,
    "preview": "--ignore-engines true"
  },
  {
    "path": "assets/css/admin.scss",
    "chars": 2113,
    "preview": "$fa-font-path: \"~font-awesome/fonts\";\n@import \"~font-awesome/scss/font-awesome.scss\";\n@import \"~bootstrap/scss/bootstrap"
  },
  {
    "path": "assets/css/app/components/_footer.scss",
    "chars": 209,
    "preview": "footer {\n  margin-top: 10px;\n  margin-bottom: 20px;\n  color: #909090;\n  a {\n    color: #666;\n  }\n  .links {\n    color: #"
  },
  {
    "path": "assets/css/app/components/_notification.scss",
    "chars": 1049,
    "preview": ".notifications {\n  .card-header {\n    font-size: 16px;\n    line-height: 32px;\n  }\n  .card-body {\n    padding-top: 0;\n   "
  },
  {
    "path": "assets/css/app/components/base.scss",
    "chars": 28921,
    "preview": "@mixin clearfix() {\n  &:before,\n  &:after {\n    content: \" \";\n    display: table;\n  }\n  &:after {\n    clear: both;\n  }\n "
  },
  {
    "path": "assets/css/app/components/page.scss",
    "chars": 1713,
    "preview": "#home_index {\n  line-height: 160%;\n}\n\n.home-icons {\n  .item {\n    text-align: center;\n    margin-bottom: 15px;\n    borde"
  },
  {
    "path": "assets/css/app/components/user.scss",
    "chars": 6117,
    "preview": ".subnav {\n  margin-bottom: -18px;\n  .nav-tabs {\n    border-bottom: 0px;\n    padding-left: 20px;\n  }\n  .nav-tabs>li>a:hov"
  },
  {
    "path": "assets/css/app.scss",
    "chars": 442,
    "preview": "$fa-font-path: \"~font-awesome/fonts\";\n@import \"~font-awesome/scss/font-awesome.scss\";\n@import \"~bootstrap/scss/bootstrap"
  },
  {
    "path": "assets/css/common/components/_header.scss",
    "chars": 3250,
    "preview": ".bd-navbar {\n  height: 50px;\n  padding: 0;\n  background: #FFF;\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n  border: 0p"
  },
  {
    "path": "assets/css/common/variables/mipha.scss",
    "chars": 1205,
    "preview": "$primary: #404040;\n$default: #e0e0e0;\n$secondary: #666;\n$gray: #F2F2F2;\n$red: #EB5424;\n$yellow: #FEE209;\n$blue: #2275da;"
  },
  {
    "path": "assets/js/admin.js",
    "chars": 107,
    "preview": "import 'phoenix_html'\nimport 'bootstrap'\n\nimport Utils from './common/components/utils'\n\nUtils.navActive()\n"
  },
  {
    "path": "assets/js/app/components/editor.js",
    "chars": 4642,
    "preview": "export default class Editor {\n  constructor() {\n    this.appendCodesFromHint()\n    this.initDropzone()\n    this.browseUp"
  },
  {
    "path": "assets/js/app/components/session.js",
    "chars": 298,
    "preview": "export default class Session {\n  static refreshExcaptcha() {\n    $('a.excaptcha-image-box').click(function(e) {\n      co"
  },
  {
    "path": "assets/js/app/components/times.js",
    "chars": 3101,
    "preview": "const moment = require('moment')\nmoment.locale('zh-cn', {\n  months: '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split(\n    '"
  },
  {
    "path": "assets/js/app/components/topic.js",
    "chars": 2984,
    "preview": "import 'jquery'\nimport 'jquery.caret'\nimport _ from 'lodash'\nimport 'static/atwho/jquery.atwho.min'\n\nconst selectorNode "
  },
  {
    "path": "assets/js/app.js",
    "chars": 668,
    "preview": "import 'phoenix_html'\nimport 'bootstrap'\nimport 'jquery.caret'\nimport 'dropzone/dist/dropzone-amd-module'\n\nimport socket"
  },
  {
    "path": "assets/js/common/components/utils.js",
    "chars": 317,
    "preview": "export default class Utils {\n  static navActive() {\n    const url =\n      window.location.protocol +\n      '//' +\n      "
  },
  {
    "path": "assets/js/socket.js",
    "chars": 1621,
    "preview": "// NOTE: The contents of this file will only be executed if\n// you uncomment its entry in \"assets/js/app.js\".\n\n// To use"
  },
  {
    "path": "assets/package.json",
    "chars": 1600,
    "preview": "{\n  \"repository\": {},\n  \"license\": \"MIT\",\n  \"engines\": {\n    \"node\": \">=9.0\",\n    \"yarn\": \">=1.6.0\"\n  },\n  \"scripts\": {\n"
  },
  {
    "path": "assets/static/robots.txt",
    "chars": 202,
    "preview": "# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file\n#\n# To ban all spiders"
  },
  {
    "path": "assets/webpack.config.js",
    "chars": 1761,
    "preview": "const path = require('path')\nconst webpack = require('webpack')\nconst MiniCssExtractPlugin = require('mini-css-extract-p"
  },
  {
    "path": "compile",
    "chars": 76,
    "preview": "cd $phoenix_dir\nnpm --prefix ./assets run deploy\nmix \"${phoenix_ex}.digest\"\n"
  },
  {
    "path": "config/config.exs",
    "chars": 2249,
    "preview": "# This file is responsible for configuring your application\n# and its dependencies with the aid of the Mix.Config module"
  },
  {
    "path": "config/dev.exs",
    "chars": 1905,
    "preview": "use Mix.Config\n\n# For development, we disable any cache and enable\n# debugging and code reloading.\n#\n# The watchers conf"
  },
  {
    "path": "config/prod.exs",
    "chars": 2725,
    "preview": "use Mix.Config\n\n# For production, we often load configuration from external\n# sources, such as your system environment. "
  },
  {
    "path": "config/test.exs",
    "chars": 513,
    "preview": "use Mix.Config\n\n# We don't run a server during test. If one is required,\n# you can enable the server option below.\nconfi"
  },
  {
    "path": "docker/docker-compose.yml",
    "chars": 194,
    "preview": "version: \"3.6\"\n\n# cd docker && docker-compose up -d\n\nservices:\n  postgresql_dev:\n    image: sameersbn/postgresql:9.6\n   "
  },
  {
    "path": "elixir_buildpack.config",
    "chars": 186,
    "preview": "# Elixir version\nelixir_version=1.8.1\n\n# Erlang version\n# available versions https://github.com/HashNuke/heroku-buildpac"
  },
  {
    "path": "lib/mipha/accounts/accounts.ex",
    "chars": 15232,
    "preview": "defmodule Mipha.Accounts do\n  @moduledoc \"\"\"\n  The Accounts context.\n  \"\"\"\n\n  import Ecto.Query, warn: false\n\n  alias Co"
  },
  {
    "path": "lib/mipha/accounts/company.ex",
    "chars": 622,
    "preview": "defmodule Mipha.Accounts.Company do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n  alias Mipha.Accounts"
  },
  {
    "path": "lib/mipha/accounts/location.ex",
    "chars": 444,
    "preview": "defmodule Mipha.Accounts.Location do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias Mipha.Accoun"
  },
  {
    "path": "lib/mipha/accounts/queries.ex",
    "chars": 501,
    "preview": "defmodule Mipha.Accounts.Queries do\n  @moduledoc false\n\n  alias Mipha.Accounts.{User, Team, Company}\n  import Ecto.Query"
  },
  {
    "path": "lib/mipha/accounts/team.ex",
    "chars": 773,
    "preview": "defmodule Mipha.Accounts.Team do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n  alias Mipha.Accounts.{U"
  },
  {
    "path": "lib/mipha/accounts/user.ex",
    "chars": 4626,
    "preview": "defmodule Mipha.Accounts.User do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n  alias Comeonin.Bcrypt\n\n"
  },
  {
    "path": "lib/mipha/accounts/user_team.ex",
    "chars": 456,
    "preview": "defmodule Mipha.Accounts.UserTeam do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n  alias Mipha.Account"
  },
  {
    "path": "lib/mipha/application.ex",
    "chars": 1243,
    "preview": "defmodule Mipha.Application do\n  @moduledoc false\n\n  use Application\n\n  # See https://hexdocs.pm/elixir/Application.html"
  },
  {
    "path": "lib/mipha/collections/collection.ex",
    "chars": 1068,
    "preview": "defmodule Mipha.Collections.Collection do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.{Changeset, Query}\n\n  alia"
  },
  {
    "path": "lib/mipha/collections/collections.ex",
    "chars": 3692,
    "preview": "defmodule Mipha.Collections do\n  @moduledoc \"\"\"\n  The Collections context.\n  \"\"\"\n\n  import Ecto.Query, warn: false\n  ali"
  },
  {
    "path": "lib/mipha/collections/queries.ex",
    "chars": 693,
    "preview": "defmodule Mipha.Collections.Queries do\n  @moduledoc false\n\n  import Ecto.Query\n  alias Mipha.Collections.Collection\n\n  @"
  },
  {
    "path": "lib/mipha/factory.ex",
    "chars": 2287,
    "preview": "defmodule Mipha.Factory do\n  @moduledoc \"\"\"\n  ExMachina Factory funcs to use in tests.\n\n  ## Examples\n\n      iex> Factor"
  },
  {
    "path": "lib/mipha/follows/follow.ex",
    "chars": 1233,
    "preview": "defmodule Mipha.Follows.Follow do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.{Changeset, Query}\n\n  alias Mipha."
  },
  {
    "path": "lib/mipha/follows/follows.ex",
    "chars": 6755,
    "preview": "defmodule Mipha.Follows do\n  @moduledoc \"\"\"\n  The Follows context.\n  \"\"\"\n\n  import Ecto.Query, warn: false\n  alias Ecto."
  },
  {
    "path": "lib/mipha/follows/queries.ex",
    "chars": 827,
    "preview": "defmodule Mipha.Follows.Queries do\n  @moduledoc \"\"\"\n  Query Follows Contetxt.\n  \"\"\"\n  import Ecto.Query\n  alias Mipha.Fo"
  },
  {
    "path": "lib/mipha/mailer.ex",
    "chars": 87,
    "preview": "defmodule Mipha.Mailer do\n  @moduledoc false\n\n  use Bamboo.Mailer, otp_app: :mipha\nend\n"
  },
  {
    "path": "lib/mipha/markdown/auto_linker.ex",
    "chars": 522,
    "preview": "defmodule Mipha.Markdown.AutoLinker do\n  @moduledoc \"\"\"\n  AutoLinker will run on a string and return the string with any"
  },
  {
    "path": "lib/mipha/markdown/embed_video_replacer.ex",
    "chars": 1363,
    "preview": "defmodule Mipha.Markdown.EmbedVideoReplacer do\n  @moduledoc false\n\n  @youtube_url_regex ~r{(\\s|^|<div>|<br>)(https?://)("
  },
  {
    "path": "lib/mipha/markdown/emoji_replacer.ex",
    "chars": 551,
    "preview": "defmodule Mipha.Markdown.EmojiReplacer do\n  @moduledoc \"\"\"\n  EmojiReplacer will run on a string and return the string wi"
  },
  {
    "path": "lib/mipha/markdown/html_renderer.ex",
    "chars": 9024,
    "preview": "# This is a clone of\n# https://raw.githubusercontent.com/pragdave/earmark/ea6382092c931ab4dd6d0dac6425430c78a61a6d/lib/e"
  },
  {
    "path": "lib/mipha/markdown/markdown_guides.md",
    "chars": 2500,
    "preview": "# Guide\n\n这是一篇讲解如何正确使用 **Markdown** 的排版示例,学会这个很有必要,能让你的文章有更佳清晰的排版。\n\n> 引用文本:Markdown is a text formatting syntax inspired\n"
  },
  {
    "path": "lib/mipha/markdown/mention_replacer.ex",
    "chars": 478,
    "preview": "defmodule Mipha.Markdown.MentionReplacer do\n  @moduledoc \"\"\"\n  Replacer\n  \"\"\"\n\n  alias Mipha.Accounts\n\n  @user_regex ~r{"
  },
  {
    "path": "lib/mipha/markdown.ex",
    "chars": 726,
    "preview": "defmodule Mipha.Markdown do\n  @moduledoc \"\"\"\n  Sanitize a string's HTML, then render the string as markdown in the Mipha"
  },
  {
    "path": "lib/mipha/notifications/notification.ex",
    "chars": 2925,
    "preview": "defmodule Mipha.Notifications.Notification do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.{Changeset, Query}\n  i"
  },
  {
    "path": "lib/mipha/notifications/notifications.ex",
    "chars": 7148,
    "preview": "defmodule Mipha.Notifications do\n  @moduledoc \"\"\"\n  The Notifications context.\n  \"\"\"\n\n  import Ecto.Query, warn: false\n "
  },
  {
    "path": "lib/mipha/notifications/queries.ex",
    "chars": 411,
    "preview": "defmodule Mipha.Notifications.Queries do\n  @moduledoc false\n\n  import Ecto.Query\n\n  alias Mipha.Accounts.User\n  alias Mi"
  },
  {
    "path": "lib/mipha/notifications/user_notification.ex",
    "chars": 1673,
    "preview": "defmodule Mipha.Notifications.UserNotification do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.{Changeset, Query}"
  },
  {
    "path": "lib/mipha/qiniu.ex",
    "chars": 498,
    "preview": "defmodule Mipha.Qiniu do\n  @moduledoc false\n\n  @base_url \"http://pbfwruvmm.bkt.clouddn.com/\"\n  @bucket \"mipha\"\n\n  def up"
  },
  {
    "path": "lib/mipha/regexp.ex",
    "chars": 217,
    "preview": "defmodule Mipha.Regexp do\n  @moduledoc \"\"\"\n  Regex\n  \"\"\"\n\n  def username do\n    ~r/^[a-z\\d](?:[a-z\\d]|-(?=[a-z\\d])){1,64"
  },
  {
    "path": "lib/mipha/replies/queries.ex",
    "chars": 818,
    "preview": "defmodule Mipha.Replies.Queries do\n  @moduledoc false\n\n  import Ecto.Query\n  alias Mipha.Replies.Reply\n\n  @doc \"\"\"\n  Ret"
  },
  {
    "path": "lib/mipha/replies/replies.ex",
    "chars": 8577,
    "preview": "defmodule Mipha.Replies do\n  @moduledoc \"\"\"\n  The Replies context.\n  \"\"\"\n\n  import Ecto.Query, warn: false\n  alias Ecto."
  },
  {
    "path": "lib/mipha/replies/reply.ex",
    "chars": 2384,
    "preview": "defmodule Mipha.Replies.Reply do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.{Changeset, Query}\n\n  alias Mipha.{"
  },
  {
    "path": "lib/mipha/repo.ex",
    "chars": 293,
    "preview": "defmodule Mipha.Repo do\n  use Ecto.Repo, otp_app: :mipha, adapter: Ecto.Adapters.Postgres\n\n  @doc \"\"\"\n  Dynamically load"
  },
  {
    "path": "lib/mipha/stars/star.ex",
    "chars": 1366,
    "preview": "defmodule Mipha.Stars.Star do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.{Changeset, Query}\n\n  alias Mipha.{\n  "
  },
  {
    "path": "lib/mipha/stars/stars.ex",
    "chars": 7015,
    "preview": "defmodule Mipha.Stars do\n  @moduledoc \"\"\"\n  The Stars context.\n  \"\"\"\n\n  import Ecto.Query, warn: false\n  alias Ecto.Mult"
  },
  {
    "path": "lib/mipha/token.ex",
    "chars": 375,
    "preview": "defmodule Mipha.Token do\n  @moduledoc false\n\n  alias Mipha.Accounts.User\n\n  @verification_salt \"mipha\"\n\n  def generate_t"
  },
  {
    "path": "lib/mipha/topics/node.ex",
    "chars": 1257,
    "preview": "defmodule Mipha.Topics.Node do\n  @moduledoc false\n  use Ecto.Schema\n  import Ecto.{Changeset, Query}\n\n  alias Mipha.Repo"
  },
  {
    "path": "lib/mipha/topics/queries.ex",
    "chars": 1620,
    "preview": "defmodule Mipha.Topics.Queries do\n  @moduledoc false\n\n  import Ecto.Query\n  alias Mipha.Topics.{Topic, Node}\n\n  @doc \"\"\""
  },
  {
    "path": "lib/mipha/topics/topic.ex",
    "chars": 4815,
    "preview": "defmodule Mipha.Topics.Topic do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.{Changeset, Query}\n  import EctoEnum"
  },
  {
    "path": "lib/mipha/topics/topics.ex",
    "chars": 7369,
    "preview": "defmodule Mipha.Topics do\n  @moduledoc \"\"\"\n  The Topics context.\n  \"\"\"\n\n  import Ecto.Query, warn: false\n  alias Ecto.Mu"
  },
  {
    "path": "lib/mipha/utils/store.ex",
    "chars": 532,
    "preview": "defmodule Mipha.Utils.Store do\n  @moduledoc \"\"\"\n  文件储存\n  \"\"\"\n\n  alias Cachex\n\n  @doc \"\"\"\n  读取文件信息\n\n  ## Example\n\n      i"
  },
  {
    "path": "lib/mipha.ex",
    "chars": 247,
    "preview": "defmodule Mipha do\n  @moduledoc \"\"\"\n  Mipha keeps the contexts that define your domain\n  and business logic.\n\n  Contexts"
  },
  {
    "path": "lib/mipha_web/channels/presence.ex",
    "chars": 2731,
    "preview": "defmodule MiphaWeb.Presence do\n  @moduledoc \"\"\"\n  Provides presence tracking to channels and processes.\n\n  See the [`Pho"
  },
  {
    "path": "lib/mipha_web/channels/room_channel.ex",
    "chars": 1240,
    "preview": "defmodule MiphaWeb.RoomChannel do\n  @moduledoc \"\"\"\n  Room Channel\n  \"\"\"\n\n  use MiphaWeb, :channel\n\n  alias MiphaWeb.Pres"
  },
  {
    "path": "lib/mipha_web/channels/topic_channel.ex",
    "chars": 863,
    "preview": "defmodule MiphaWeb.TopicChannel do\n  @moduledoc \"\"\"\n  Topic Channel\n  \"\"\"\n\n  use MiphaWeb, :channel\n\n  def join(\"topic:\""
  },
  {
    "path": "lib/mipha_web/channels/user_socket.ex",
    "chars": 1475,
    "preview": "defmodule MiphaWeb.UserSocket do\n  use Phoenix.Socket\n\n  ## Channels\n  # channel \"room:*\", MiphaWeb.RoomChannel\n  channe"
  },
  {
    "path": "lib/mipha_web/controllers/admin/company_controller.ex",
    "chars": 572,
    "preview": "defmodule MiphaWeb.Admin.CompanyController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Accounts\n  alias Mipha.Accounts"
  },
  {
    "path": "lib/mipha_web/controllers/admin/node_controller.ex",
    "chars": 1742,
    "preview": "defmodule MiphaWeb.Admin.NodeController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Topics\n  alias Mipha.Topics.Querie"
  },
  {
    "path": "lib/mipha_web/controllers/admin/notification_controller.ex",
    "chars": 725,
    "preview": "defmodule MiphaWeb.Admin.NotificationController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Notifications\n\n  def index"
  },
  {
    "path": "lib/mipha_web/controllers/admin/page_controller.ex",
    "chars": 131,
    "preview": "defmodule MiphaWeb.Admin.PageController do\n  use MiphaWeb, :controller\n\n  def index(conn, _) do\n    render(conn, :index)"
  },
  {
    "path": "lib/mipha_web/controllers/admin/reply_controller.ex",
    "chars": 669,
    "preview": "defmodule MiphaWeb.Admin.ReplyController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Replies\n  alias Mipha.Replies.Que"
  },
  {
    "path": "lib/mipha_web/controllers/admin/team_controller.ex",
    "chars": 540,
    "preview": "defmodule MiphaWeb.Admin.TeamController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Accounts\n  alias Mipha.Accounts.Qu"
  },
  {
    "path": "lib/mipha_web/controllers/admin/topic_controller.ex",
    "chars": 279,
    "preview": "defmodule MiphaWeb.Admin.TopicController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Topics.Queries\n\n  def index(conn,"
  },
  {
    "path": "lib/mipha_web/controllers/admin/user_controller.ex",
    "chars": 540,
    "preview": "defmodule MiphaWeb.Admin.UserController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Accounts\n  alias Mipha.Accounts.Qu"
  },
  {
    "path": "lib/mipha_web/controllers/auth_controller.ex",
    "chars": 2605,
    "preview": "defmodule MiphaWeb.AuthController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Accounts\n\n  plug :authorized_user when a"
  },
  {
    "path": "lib/mipha_web/controllers/callback_controller.ex",
    "chars": 363,
    "preview": "defmodule MiphaWeb.CallbackController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Qiniu\n\n  @doc \"\"\"\n  Upload image cal"
  },
  {
    "path": "lib/mipha_web/controllers/company_controller.ex",
    "chars": 140,
    "preview": "defmodule MiphaWeb.CompanyController do\n  use MiphaWeb, :controller\n\n  def action(conn, _) do\n    render(conn, action_na"
  },
  {
    "path": "lib/mipha_web/controllers/location_controller.ex",
    "chars": 281,
    "preview": "defmodule MiphaWeb.LocationController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Accounts\n\n  def index(conn, _) do\n  "
  },
  {
    "path": "lib/mipha_web/controllers/notification_controller.ex",
    "chars": 907,
    "preview": "defmodule MiphaWeb.NotificationController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Notifications\n\n  plug MiphaWeb.P"
  },
  {
    "path": "lib/mipha_web/controllers/page_controller.ex",
    "chars": 280,
    "preview": "defmodule MiphaWeb.PageController do\n  use MiphaWeb, :controller\n\n  alias Mipha.Markdown\n\n  def index(conn, _params) do\n"
  },
  {
    "path": "lib/mipha_web/controllers/reply_controller.ex",
    "chars": 3024,
    "preview": "defmodule MiphaWeb.ReplyController do\n  use MiphaWeb, :controller\n\n  import MiphaWeb.Endpoint, only: [broadcast!: 3]\n  a"
  },
  {
    "path": "lib/mipha_web/controllers/search_controller.ex",
    "chars": 441,
    "preview": "defmodule MiphaWeb.SearchController do\n  use MiphaWeb, :controller\n\n  alias Mipha.{Accounts, Qiniu}\n\n  def index(conn, _"
  },
  {
    "path": "lib/mipha_web/controllers/session_controller.ex",
    "chars": 1955,
    "preview": "defmodule MiphaWeb.SessionController do\n  use MiphaWeb, :controller\n\n  alias Mipha.{Accounts, Mailer, Token}\n  alias Mip"
  },
  {
    "path": "lib/mipha_web/controllers/setting_controller.ex",
    "chars": 3467,
    "preview": "defmodule MiphaWeb.SettingController do\n  use MiphaWeb, :controller\n\n  alias Mipha.{Accounts, Qiniu}\n\n  plug MiphaWeb.Pl"
  },
  {
    "path": "lib/mipha_web/controllers/team_controller.ex",
    "chars": 659,
    "preview": "defmodule MiphaWeb.TeamController do\n  use MiphaWeb, :controller\n\n  alias Mipha.{Accounts, Topics}\n  alias Topics.Querie"
  },
  {
    "path": "lib/mipha_web/controllers/topic_controller.ex",
    "chars": 7266,
    "preview": "defmodule MiphaWeb.TopicController do\n  use MiphaWeb, :controller\n\n  alias Mipha.{Topics, Stars, Collections, Markdown}\n"
  },
  {
    "path": "lib/mipha_web/controllers/user_controller.ex",
    "chars": 6020,
    "preview": "defmodule MiphaWeb.UserController do\n  use MiphaWeb, :controller\n\n  alias Mipha.{Mailer, Accounts, Topics, Replies, Foll"
  },
  {
    "path": "lib/mipha_web/email.ex",
    "chars": 1058,
    "preview": "defmodule MiphaWeb.Email do\n  @moduledoc \"\"\"\n  发送邮件集。\n  \"\"\"\n\n  use Bamboo.Phoenix, view: MiphaWeb.EmailView\n\n  @from \"an"
  },
  {
    "path": "lib/mipha_web/endpoint.ex",
    "chars": 1758,
    "preview": "defmodule MiphaWeb.Endpoint do\n  use Phoenix.Endpoint, otp_app: :mipha\n\n  socket \"/socket\", MiphaWeb.UserSocket,\n    # o"
  },
  {
    "path": "lib/mipha_web/gettext.ex",
    "chars": 702,
    "preview": "defmodule MiphaWeb.Gettext do\n  @moduledoc \"\"\"\n  A module providing Internationalization with a gettext-based API.\n\n  By"
  },
  {
    "path": "lib/mipha_web/plugs/attack.ex",
    "chars": 1811,
    "preview": "defmodule MiphaWeb.Plug.Attack do\n  @moduledoc \"\"\"\n  attack\n  \"\"\"\n\n  import Plug.Conn\n  use PlugAttack\n\n  rule(\"throttle"
  },
  {
    "path": "lib/mipha_web/plugs/current_user.ex",
    "chars": 392,
    "preview": "defmodule MiphaWeb.Plug.CurrentUser do\n  @moduledoc \"\"\"\n  A `Plug` to assign `:current_user` && `:user_token` based on t"
  },
  {
    "path": "lib/mipha_web/plugs/locale.ex",
    "chars": 392,
    "preview": "defmodule MiphaWeb.Plug.Locale do\n  @moduledoc \"\"\"\n  Locale, support: 简体中文 and English\n  \"\"\"\n\n  import Plug.Conn\n\n  def "
  },
  {
    "path": "lib/mipha_web/plugs/require_admin.ex",
    "chars": 431,
    "preview": "defmodule MiphaWeb.Plug.RequireAdmin do\n  @moduledoc false\n\n  import Plug.Conn\n  import Phoenix.Controller\n\n  alias Miph"
  },
  {
    "path": "lib/mipha_web/plugs/require_user.ex",
    "chars": 551,
    "preview": "defmodule MiphaWeb.Plug.RequireUser do\n  @moduledoc \"\"\"\n  A `Plug` to redirect to `pages/index` if there is no current u"
  },
  {
    "path": "lib/mipha_web/router.ex",
    "chars": 5217,
    "preview": "defmodule MiphaWeb.Router do\n  use MiphaWeb, :router\n  use Plug.ErrorHandler\n  use Sentry.Plug\n\n  pipeline :browser do\n "
  },
  {
    "path": "lib/mipha_web/session.ex",
    "chars": 1011,
    "preview": "defmodule MiphaWeb.Session do\n  @moduledoc \"\"\"\n  Some helpers for session-related things\n  \"\"\"\n\n  alias Mipha.Accounts\n "
  },
  {
    "path": "lib/mipha_web/templates/admin/company/index.html.eex",
    "chars": 1399,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <div class=\"toolbar\">\n      <%= form_for @conn, admin_company_"
  },
  {
    "path": "lib/mipha_web/templates/admin/node/edit.html.eex",
    "chars": 167,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <%= render \"form.html\", Map.put(assigns, :action, admin_node_p"
  },
  {
    "path": "lib/mipha_web/templates/admin/node/form.html.eex",
    "chars": 1034,
    "preview": "<%= form_for @changeset, @action, fn f -> %>\n  <%= if @changeset.action do %>\n    <div class=\"alert alert-danger\">\n     "
  },
  {
    "path": "lib/mipha_web/templates/admin/node/index.html.eex",
    "chars": 1679,
    "preview": "<div class=\"card bg-light\">\n  <span><%= link \"新建节点\", to: admin_node_path(@conn, :new), class: \"btn btn-default btn-sm\" %"
  },
  {
    "path": "lib/mipha_web/templates/admin/node/new.html.eex",
    "chars": 162,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <%= render \"form.html\", Map.put(assigns, :action, admin_node_p"
  },
  {
    "path": "lib/mipha_web/templates/admin/node/show.html.eex",
    "chars": 429,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <ul>\n      <li>\n        <strong>Name:</strong>\n        <%= @no"
  },
  {
    "path": "lib/mipha_web/templates/admin/notification/index.html.eex",
    "chars": 1175,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <table class=\"table table-bordered table-hover table-sm text-c"
  },
  {
    "path": "lib/mipha_web/templates/admin/notification/show.html.eex",
    "chars": 594,
    "preview": "<h2>Show Notification</h2>\n\n<ul>\n\n  <li>\n    <strong>Action:</strong>\n    <%= @notification.action %>\n  </li>\n\n  <li>\n  "
  },
  {
    "path": "lib/mipha_web/templates/admin/page/index.html.eex",
    "chars": 98,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <h3>Admin Dashboard</h3>\n  </div>\n</div>"
  },
  {
    "path": "lib/mipha_web/templates/admin/reply/index.html.eex",
    "chars": 1510,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <div class=\"toolbar\">\n      <%= form_for @conn, admin_reply_pa"
  },
  {
    "path": "lib/mipha_web/templates/admin/reply/show.html.eex",
    "chars": 435,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <ul>\n      <li>\n        <strong>Topic:</strong>\n        <%= @r"
  },
  {
    "path": "lib/mipha_web/templates/admin/team/edit.html.eex",
    "chars": 167,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <%= render \"form.html\", Map.put(assigns, :action, admin_team_p"
  },
  {
    "path": "lib/mipha_web/templates/admin/team/form.html.eex",
    "chars": 1221,
    "preview": "<%= form_for @changeset, @action, fn f -> %>\n  <%= if @changeset.action do %>\n    <div class=\"alert alert-danger\">\n     "
  },
  {
    "path": "lib/mipha_web/templates/admin/team/index.html.eex",
    "chars": 1360,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <div class=\"toolbar\">\n      <%= form_for @conn, admin_team_pat"
  },
  {
    "path": "lib/mipha_web/templates/admin/team/new.html.eex",
    "chars": 160,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <%= render \"form.html\", Map.put(assigns, :action, admin_team_p"
  },
  {
    "path": "lib/mipha_web/templates/admin/team/show.html.eex",
    "chars": 522,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <ul>\n      <li>\n        <strong>Owner:</strong>\n        <%= @t"
  },
  {
    "path": "lib/mipha_web/templates/admin/topic/index.html.eex",
    "chars": 1538,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <div class=\"toolbar\">\n      <%= form_for @conn, admin_topic_pa"
  },
  {
    "path": "lib/mipha_web/templates/admin/user/index.html.eex",
    "chars": 1228,
    "preview": "<div class=\"card bg-light\">\n  <div class=\"card-body\">\n    <div class=\"toolbar\">\n      <%= form_for @conn, admin_user_pat"
  },
  {
    "path": "lib/mipha_web/templates/auth/login.html.eex",
    "chars": 1996,
    "preview": "<div class=\"row\">\n  <div class=\"col\"></div>\n  <div class=\"col-md-5\">\n    <div class=\"card\">\n      <div class=\"card-heade"
  },
  {
    "path": "lib/mipha_web/templates/company/index.html.eex",
    "chars": 27,
    "preview": "<h1>Company Index Page</h1>"
  },
  {
    "path": "lib/mipha_web/templates/company/show.html.eex",
    "chars": 26,
    "preview": "<h1>Company Show Page</h1>"
  },
  {
    "path": "lib/mipha_web/templates/email/forgot_password.html.eex",
    "chars": 437,
    "preview": "<p class=\"lead\">你好 <%= @user.username %></p>\n<p>有人请求找回在 Elixir Mipha 上面,账号为 <span class=\"text-danger\"><%= @user.username"
  },
  {
    "path": "lib/mipha_web/templates/email/verify_email.html.eex",
    "chars": 346,
    "preview": "<p class=\"lead\">欢迎 <%= @user.username %></p>\n<p>你已经成功注册 Elixir Mipha 的账号,\n  接下来你需要点击下面的连接 <span class=\"text-danger\">激活</"
  },
  {
    "path": "lib/mipha_web/templates/email/welcome.html.eex",
    "chars": 343,
    "preview": "<p class=\"lead\">你好 <%= @user.username %></p>\n<p>你已经成功在 <%= link \"Elixir Mipha\", to: topic_url(MiphaWeb.Endpoint, :index)"
  },
  {
    "path": "lib/mipha_web/templates/layout/_flash.html.eex",
    "chars": 469,
    "preview": "<div class=\"container\" style=\"margin-top: 10px;\">\n  <%= for item <- ~w(danger success warning info)a do %>\n    <%= if ge"
  },
  {
    "path": "lib/mipha_web/templates/layout/_footer.html.eex",
    "chars": 119,
    "preview": "<footer class=\"footer\" id=\"footer\">\n  <div class=\"container\">\n    <div id=\"online-users\">\n    </div>\n  </div>\n</footer>"
  },
  {
    "path": "lib/mipha_web/templates/layout/_header.html.eex",
    "chars": 3565,
    "preview": "<div class=\"header navbar navbar-expand flex-md-row bd-navbar\">\n  <div class=\"container d-sm-flex\">\n    <div class=\"navb"
  },
  {
    "path": "lib/mipha_web/templates/layout/_navbar.html.eex",
    "chars": 1291,
    "preview": " <div class=\"header\">\n    <div class=\"header navbar navbar-expand flex-md-row bd-navbar\">\n      <div class=\"container d-"
  },
  {
    "path": "lib/mipha_web/templates/layout/_sub_header.html.eex",
    "chars": 724,
    "preview": "<div class=\"sub-navbar node-header hide-ios\">\n  <div class=\"container\">\n    <%= render MiphaWeb.TopicView, \"_node_select"
  },
  {
    "path": "lib/mipha_web/templates/layout/admin.html.eex",
    "chars": 693,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE="
  },
  {
    "path": "lib/mipha_web/templates/layout/app.html.eex",
    "chars": 997,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE="
  },
  {
    "path": "lib/mipha_web/templates/layout/email.html.eex",
    "chars": 79,
    "preview": "<html>\n  <head>\n  </head>\n  <body>\n    <%= @inner_content %>\n  </body>\n</html>\n"
  },
  {
    "path": "lib/mipha_web/templates/location/index.html.eex",
    "chars": 29,
    "preview": "<h1>Locations Index Page</h1>"
  },
  {
    "path": "lib/mipha_web/templates/location/show.html.eex",
    "chars": 630,
    "preview": "<div id=\"hot_users\" class=\"card user-list\">\n  <div class=\"card-header\"><%= @location.name %>的伙伴</div>\n  <div class=\"card"
  },
  {
    "path": "lib/mipha_web/templates/notification/_followed.html.eex",
    "chars": 177,
    "preview": "<%= if is_nil(@target_object) do %>\n  <div class=\"media-body\">相关信息已删除</div>\n<% else %>\n  <div class=\"media-heading\">\n   "
  },
  {
    "path": "lib/mipha_web/templates/notification/_reply_comment_added.html.eex",
    "chars": 378,
    "preview": "<%= if is_nil(@target_object) || is_nil(@target_object.topic) do %>\n  <div class=\"media-body\">相关信息已删除</div>\n<% else %>\n "
  },
  {
    "path": "lib/mipha_web/templates/notification/_reply_mentioned.html.eex",
    "chars": 355,
    "preview": "<%= if is_nil(@target_object) || is_nil(@target_object.topic) do %>\n  <div class=\"media-body\">相关信息已删除</div>\n<% else %>\n "
  },
  {
    "path": "lib/mipha_web/templates/notification/_reply_starred.html.eex",
    "chars": 246,
    "preview": "<%= if is_nil(@target_object) || is_nil(@target_object.topic) do %>\n  <div class=\"media-body\">相关信息已删除</div>\n<% else %>\n "
  },
  {
    "path": "lib/mipha_web/templates/notification/_topic_added.html.eex",
    "chars": 184,
    "preview": "<%= if is_nil(@target_object) do %>\n  <div class=\"media-body\">相关信息已删除</div>\n<% else %>\n  <div class=\"media-heading\">\n   "
  },
  {
    "path": "lib/mipha_web/templates/notification/_topic_mentioned.html.eex",
    "chars": 314,
    "preview": "<% if is_nil(@target_object) do %>\n  <div class=\"media-body\">相关信息已删除</div>\n<% else %>\n  <div class=\"media-heading\">\n    "
  },
  {
    "path": "lib/mipha_web/templates/notification/_topic_reply_added.html.eex",
    "chars": 357,
    "preview": "<%= if is_nil(@target_object) || is_nil(@target_object.topic) do %>\n  <div class=\"media-body\">相关信息已删除</div>\n<% else %>\n "
  },
  {
    "path": "lib/mipha_web/templates/notification/_topic_starred.html.eex",
    "chars": 158,
    "preview": "<%= if is_nil(@target_object) do %>\n  相关信息已删除\n<% else %>\n  <div class=\"media-heading\">\n    你的帖子 <%= topic_title_tag(@tar"
  },
  {
    "path": "lib/mipha_web/templates/notification/index.html.eex",
    "chars": 1432,
    "preview": "<div class=\"notifications card\">\n  <div class=\"card-header hide-ios clearfix\">\n    <%= gettext \"Notifications\" %>\n    <s"
  },
  {
    "path": "lib/mipha_web/templates/page/_locations.html.eex",
    "chars": 346,
    "preview": "<div class=\"index-locations card\">\n  <div class=\"card-header\">\n    <%= gettext \"Hot Cities\" %>\n  </div>\n  <div class=\"ca"
  },
  {
    "path": "lib/mipha_web/templates/page/index.html.eex",
    "chars": 81,
    "preview": "<%#= render \"_nodes.html\", assigns %>\n<%#= render \"_locations.html\", assigns %>\n\n"
  },
  {
    "path": "lib/mipha_web/templates/page/markdown.html.eex",
    "chars": 320,
    "preview": "<div class=\"card markdown\">\n  <div class=\"card-body\">\n    <h1><%= gettext \"Markdown Guide\" %></h1>\n    <div class=\"row\">"
  },
  {
    "path": "lib/mipha_web/templates/reply/edit.html.eex",
    "chars": 1039,
    "preview": "<div>\n  <div class=\"edit-reply card\">\n    <div class=\"card-header\">\n      <%= gettext \"Edit Reply\" %>\n      <span class="
  },
  {
    "path": "lib/mipha_web/templates/search/index.html.eex",
    "chars": 48,
    "preview": "<div class=\"card\">\n  <h1>Search Page</h1>\n</div>"
  },
  {
    "path": "lib/mipha_web/templates/session/new.html.eex",
    "chars": 1919,
    "preview": "<div class=\"row\">\n  <div class=\"col\"></div>\n  <div class=\"col-lg-6\">\n    <div class=\"card\">\n      <div class=\"card-heade"
  },
  {
    "path": "lib/mipha_web/templates/setting/_sidebar.html.eex",
    "chars": 1223,
    "preview": "<div class=\"card\">\n  <ul class=\"nav nav-pills flex-column nav-stacked mi-nav\">\n    <li class=\"nav-item\">\n      <%= link "
  },
  {
    "path": "lib/mipha_web/templates/setting/account.html.eex",
    "chars": 1269,
    "preview": "<div class=\"row\">\n  <div class=\"col-sm-2 col-md-offset-1 setting-menu\">\n    <%= render \"_sidebar.html\", assigns %>\n  </d"
  },
  {
    "path": "lib/mipha_web/templates/setting/password.html.eex",
    "chars": 1482,
    "preview": "<div class=\"row\">\n  <div class=\"col-sm-2 col-md-offset-1 setting-menu\">\n    <%= render \"_sidebar.html\", assigns %>\n  </d"
  },
  {
    "path": "lib/mipha_web/templates/setting/profile.html.eex",
    "chars": 1507,
    "preview": "<div class=\"row\">\n  <div class=\"col-sm-2 col-md-offset-1 setting-menu\">\n    <%= render \"_sidebar.html\", assigns %>\n  </d"
  },
  {
    "path": "lib/mipha_web/templates/setting/reward.html.eex",
    "chars": 1587,
    "preview": "<div class=\"row\">\n  <div class=\"col-sm-2 col-md-offset-1 setting-menu\">\n    <%= render \"_sidebar.html\", assigns %>\n  </d"
  },
  {
    "path": "lib/mipha_web/templates/setting/show.html.eex",
    "chars": 1597,
    "preview": "<div class=\"row\">\n  <div class=\"col-sm-2 col-md-offset-1 setting-menu\">\n    <%= render \"_sidebar.html\", assigns %>\n  </d"
  },
  {
    "path": "lib/mipha_web/templates/shared/_pagination.html.eex",
    "chars": 137,
    "preview": "<%= if @paginate.total_pages > 1 do %>\n  <div class=\"pagination\">\n    <%= turbo_pagination_links(@conn, @paginate) %>\n  "
  },
  {
    "path": "lib/mipha_web/templates/team/_nav.html.eex",
    "chars": 302,
    "preview": "<ul class=\"nav nav-tabs team-menu mi-nav\">\n  <li class=\"nav-item\">\n    <%= link gettext(\"Team Topics\"), to: team_path(@c"
  },
  {
    "path": "lib/mipha_web/templates/team/index.html.eex",
    "chars": 24,
    "preview": "<h1>Team Index Page</h1>"
  },
  {
    "path": "lib/mipha_web/templates/team/people.html.eex",
    "chars": 694,
    "preview": "<%= render \"_nav.html\", assigns %>\n\n<div class=\"card team-users\">\n  <div class=\"card-body\">\n    <table class=\"table tabl"
  },
  {
    "path": "lib/mipha_web/templates/team/show.html.eex",
    "chars": 1053,
    "preview": "<%= render \"_nav.html\", assigns %>\n\n<div class=\"row\">\n  <div class=\"col-md-8\">\n    <div class=\"card topics\">\n      <div "
  },
  {
    "path": "lib/mipha_web/templates/topic/_editor_toolbar.html.eex",
    "chars": 1627,
    "preview": "<div class=\"editor-toolbar clearfix\">\n  <div class=\"reply-to pull-right\" style=\"display:none;\">\n    <i class=\"fa fa-mail"
  },
  {
    "path": "lib/mipha_web/templates/topic/_form.html.eex",
    "chars": 1308,
    "preview": "<div class=\"card-body\">\n  <%= render \"_node_selector.html\", assigns %>\n  <%= form_for @changeset, @action, fn f -> %>\n  "
  },
  {
    "path": "lib/mipha_web/templates/topic/_handle_reply.html.eex",
    "chars": 1563,
    "preview": "<%= if @current_user do %>\n  <%= if is_nil(@topic.closed_at) do %>\n    <div id=\"reply\" class=\"card\">\n      <div class=\"c"
  },
  {
    "path": "lib/mipha_web/templates/topic/_need_register_or_login.html.eex",
    "chars": 353,
    "preview": "<div class=\"card\">\n  <div class=\"card-body\">\n    <div id=\"reply\" class=\"form box\">\n      <div style=\"padding:20px;\" data"
  },
  {
    "path": "lib/mipha_web/templates/topic/_node_selector.html.eex",
    "chars": 1330,
    "preview": "<div class=\"modal\" id=\"node-selector\" tabindex=\"-1\" role=\"dialog\" style=\"display: none;\" aria-hidden=\"true\">\n  <div clas"
  },
  {
    "path": "lib/mipha_web/templates/topic/_nodes.html.eex",
    "chars": 616,
    "preview": "<div class=\"index-sections card\">\n  <div class=\"card-header\"><%= gettext \"Nodes\" %></div>\n  <div class=\"card-body\">\n    "
  },
  {
    "path": "lib/mipha_web/templates/topic/_operate_toolbar.html.eex",
    "chars": 2667,
    "preview": "<div class=\"opts\">\n  <%= if has_starred?(user: @current_user, topic: @topic) do %>\n    <%= link to: topic_path(@conn, :u"
  },
  {
    "path": "lib/mipha_web/templates/topic/_relation_topic.html.eex",
    "chars": 563,
    "preview": "<div class=\"card\">\n  <div class=\"card-header\">相关话题</div>\n  <ul class=\"list-group list-group-flush\">\n    <li class=\"list-"
  },
  {
    "path": "lib/mipha_web/templates/topic/_replies.html.eex",
    "chars": 2886,
    "preview": "<div id=\"replies\" class=\"card\">\n  <div class=\"total card-header hide-ios\">\n    <%= gettext \"Total\" %>\n    <b><%= @topic."
  },
  {
    "path": "lib/mipha_web/templates/topic/_right.html.eex",
    "chars": 2291,
    "preview": "<div class=\"card\">\n  <div class=\"card-body\">\n    <a class=\"btn btn-primary btn-block\" href=\"/topics/new\"><%= gettext \"Ne"
  },
  {
    "path": "lib/mipha_web/templates/topic/_right_sidebar.html.eex",
    "chars": 2105,
    "preview": "<div id=\"topic-sidebar\" data-spy=\"affix\" data-offset-bottom=\"65\">\n  <div class=\"card\">\n    <div class=\"card-body\">\n     "
  },
  {
    "path": "lib/mipha_web/templates/topic/_topic.html.eex",
    "chars": 1577,
    "preview": "<div class=\"topic media\">\n  <div class=\"avatar\">\n    <%= link to: user_path(MiphaWeb.Endpoint, :show, @item.user.usernam"
  },
  {
    "path": "lib/mipha_web/templates/topic/_topics.html.eex",
    "chars": 316,
    "preview": "<div class=\"topics topics-index card\">\n  <div class=\"card-body item-list\">\n    <%= for item <- @topics do %>\n      <%= r"
  },
  {
    "path": "lib/mipha_web/templates/topic/edit.html.eex",
    "chars": 196,
    "preview": "<div class=\"card\">\n  <div class=\"card-header hide-ios\">\n    <%= gettext \"Edit Topic\" %>\n  </div>\n  <%= render \"_form.htm"
  },
  {
    "path": "lib/mipha_web/templates/topic/educational.html.eex",
    "chars": 225,
    "preview": "<div class=\"row\">\n  <div class=\"col-md-9\">\n    <%= render \"_topics.html\", assigns %>\n    <%= render \"_nodes.html\", assig"
  },
  {
    "path": "lib/mipha_web/templates/topic/featured.html.eex",
    "chars": 225,
    "preview": "<div class=\"row\">\n  <div class=\"col-md-9\">\n    <%= render \"_topics.html\", assigns %>\n    <%= render \"_nodes.html\", assig"
  },
  {
    "path": "lib/mipha_web/templates/topic/index.html.eex",
    "chars": 225,
    "preview": "<div class=\"row\">\n  <div class=\"col-md-9\">\n    <%= render \"_topics.html\", assigns %>\n    <%= render \"_nodes.html\", assig"
  },
  {
    "path": "lib/mipha_web/templates/topic/jobs.html.eex",
    "chars": 225,
    "preview": "<div class=\"row\">\n  <div class=\"col-md-9\">\n    <%= render \"_topics.html\", assigns %>\n    <%= render \"_nodes.html\", assig"
  },
  {
    "path": "lib/mipha_web/templates/topic/new.html.eex",
    "chars": 187,
    "preview": "<div class=\"card\">\n  <div class=\"card-header hide-ios\">\n    <%= gettext \"New Topic\" %>\n  </div>\n  <%= render \"_form.html"
  },
  {
    "path": "lib/mipha_web/templates/topic/no_reply.html.eex",
    "chars": 225,
    "preview": "<div class=\"row\">\n  <div class=\"col-md-9\">\n    <%= render \"_topics.html\", assigns %>\n    <%= render \"_nodes.html\", assig"
  },
  {
    "path": "lib/mipha_web/templates/topic/popular.html.eex",
    "chars": 225,
    "preview": "<div class=\"row\">\n  <div class=\"col-md-9\">\n    <%= render \"_topics.html\", assigns %>\n    <%= render \"_nodes.html\", assig"
  },
  {
    "path": "lib/mipha_web/templates/topic/show.html.eex",
    "chars": 2143,
    "preview": "<script>window.channelTopicId = \"<%= @topic.id %>\"</script>\n<div class=\"row\">\n  <div class=\"col-md-9\">\n    <div class=\"t"
  }
]

// ... and 119 more files (download for full content)

About this extraction

This page contains the full source code of the zven21/mipha GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 319 files (451.9 KB), approximately 142.5k tokens, and a symbol index with 699 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!