diff --git a/.eslintignore b/.eslintignore index 4447ad67265..885e2cf3c7c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,3 +12,4 @@ node_modules/ spec/ dist/ tmp/ +documentation/ diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index aec086257ae..eface1eba7f 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -7,7 +7,7 @@ on: - main permissions: - contents: read + contents: write jobs: build: @@ -40,7 +40,6 @@ jobs: gem install bundler --conservative -v $(awk '/BUNDLED WITH/ { getline; gsub(/ /,""); print $0 }' Gemfile.lock) bundle config --local path vendor/bundle bundle config --local deployment true - bundle config --local without development bundle install --jobs 4 bundle clean @@ -60,17 +59,23 @@ jobs: - name: Yarn install run: yarn install - - name: Check Chat documentation + - name: Check documentation run: | - LOAD_PLUGINS=1 bin/rake chat:doc + LOAD_PLUGINS=1 bin/rake documentation - if [ ! -z "$(git status --porcelain plugins/chat/docs/)" ]; then - echo "Chat documentation is not up to date. To resolve, run:" - echo " LOAD_PLUGINS=1 bin/rake chat:doc" + if [ ! -z "$(git status --porcelain documentation/)" ]; then + echo "Documentation is not up to date. To resolve, run:" + echo " LOAD_PLUGINS=1 bin/rake documentation" echo echo "Or manually apply the diff printed below:" echo "---------------------------------------------" - git -c color.ui=always diff plugins/chat/docs/ + git -c color.ui=always diff documentation/ exit 1 fi timeout-minutes: 30 + + - name: Deploy documentation to github pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./documentation diff --git a/.jsdoc b/.jsdoc index 92345fda22c..06d1cf60830 100644 --- a/.jsdoc +++ b/.jsdoc @@ -3,5 +3,19 @@ { "source": { "excludePattern": "" + }, + "templates": { + "default": { + "includeDate": false + } + }, + "opts": { + "template": "./node_modules/tidy-jsdoc", + "prism-theme": "prism-custom", + "encoding": "utf8", + "recurse": true + }, + "metadata": { + "title": "Discourse" } } diff --git a/.prettierignore b/.prettierignore index 93b35f929e2..c1caccc0683 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,7 @@ plugins/**/assets/stylesheets/vendor/ plugins/**/assets/javascripts/vendor/ plugins/**/config/locales/**/*.yml plugins/**/config/*.yml +documentation/ package.json config/locales/**/*.yml !config/locales/**/*.en*.yml diff --git a/Gemfile b/Gemfile index 93cb5e09da0..cbb730e2620 100644 --- a/Gemfile +++ b/Gemfile @@ -180,6 +180,7 @@ group :development do gem "better_errors", platform: :mri, require: !!ENV["BETTER_ERRORS"] gem "binding_of_caller" gem "yaml-lint" + gem "yard" end if ENV["ALLOW_DEV_POPULATE"] == "1" diff --git a/Gemfile.lock b/Gemfile.lock index 6227eb13c8f..027298a07e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -516,6 +516,8 @@ GEM xpath (3.2.0) nokogiri (~> 1.8) yaml-lint (0.1.2) + yard (0.9.28) + webrick (~> 1.7.0) zeitwerk (2.6.7) PLATFORMS @@ -663,6 +665,7 @@ DEPENDENCIES webrick xorcist yaml-lint + yard BUNDLED WITH 2.4.4 diff --git a/documentation/assets/blob-wide-yellow3.svg b/documentation/assets/blob-wide-yellow3.svg new file mode 100644 index 00000000000..b3aa7ca2591 --- /dev/null +++ b/documentation/assets/blob-wide-yellow3.svg @@ -0,0 +1,3 @@ + + + diff --git a/documentation/assets/blobs.svg b/documentation/assets/blobs.svg new file mode 100644 index 00000000000..893715168fd --- /dev/null +++ b/documentation/assets/blobs.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/documentation/assets/favicon.ico b/documentation/assets/favicon.ico new file mode 100644 index 00000000000..ae73567c642 Binary files /dev/null and b/documentation/assets/favicon.ico differ diff --git a/documentation/assets/favicon.png b/documentation/assets/favicon.png new file mode 100644 index 00000000000..90b9b30dc4b Binary files /dev/null and b/documentation/assets/favicon.png differ diff --git a/documentation/assets/highlight.png b/documentation/assets/highlight.png new file mode 100644 index 00000000000..87a36491d0f Binary files /dev/null and b/documentation/assets/highlight.png differ diff --git a/documentation/assets/logo.svg b/documentation/assets/logo.svg new file mode 100644 index 00000000000..78fd5db6d43 --- /dev/null +++ b/documentation/assets/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/documentation/chat/backend/Chat.html b/documentation/chat/backend/Chat.html new file mode 100644 index 00000000000..56d435b47a7 --- /dev/null +++ b/documentation/chat/backend/Chat.html @@ -0,0 +1,118 @@ + + + + + + + Module: Chat + + — Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: Chat + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
plugins/chat/app/services/base.rb,
+ plugins/chat/app/services/update_user_last_read.rb,
plugins/chat/app/services/trash_channel.rb,
plugins/chat/app/services/update_channel.rb,
plugins/chat/app/services/update_channel_status.rb
+
+
+ +
+ +

Defined Under Namespace

+

+ + + Modules: Service + + + + +

+ + + + + + + + + +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/backend/Chat/Service.html b/documentation/chat/backend/Chat/Service.html new file mode 100644 index 00000000000..fb8447dab22 --- /dev/null +++ b/documentation/chat/backend/Chat/Service.html @@ -0,0 +1,120 @@ + + + + + + + Module: Chat::Service + + — Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: Chat::Service + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
plugins/chat/app/services/base.rb,
+ plugins/chat/app/services/update_user_last_read.rb,
plugins/chat/app/services/trash_channel.rb,
plugins/chat/app/services/update_channel.rb,
plugins/chat/app/services/update_channel_status.rb
+
+
+ +
+ +

Defined Under Namespace

+

+ + + Modules: Base + + + + Classes: TrashChannel, UpdateChannel, UpdateChannelStatus, UpdateUserLastRead + + +

+ + + + + + + + + +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/backend/Chat/Service/Base.html b/documentation/chat/backend/Chat/Service/Base.html new file mode 100644 index 00000000000..2d2f2254179 --- /dev/null +++ b/documentation/chat/backend/Chat/Service/Base.html @@ -0,0 +1,723 @@ + + + + + + + Module: Chat::Service::Base + + — Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: Chat::Service::Base + + + +

+
+ + + + +
+
Extended by:
+
ActiveSupport::Concern
+
+ + + + + + +
+
Included in:
+
TrashChannel, UpdateChannel, UpdateChannelStatus, UpdateUserLastRead
+
+ + + +
+
Defined in:
+
plugins/chat/app/services/base.rb
+
+ +
+ +

Overview

+
+ +

Module to be included to provide steps DSL to any class. This allows to create easy to understand services as the whole service cycle is visible simply by reading the beginning of its class.

+ +

Steps are executed in the order they’re defined. They will use their name to execute the corresponding method defined in the service class.

+ +

Currently, there are 5 types of steps:

+
  • +

    model(name = :model): used to instantiate a model (either by building it or fetching it from the DB). If a falsy value is returned, then the step will fail. Otherwise the resulting object will be assigned in context[name] (context[:model] by default).

    +
  • +

    policy(name = :default): used to perform a check on the state of the system. Typically used to run guardians. If a falsy value is returned, the step will fail.

    +
  • +

    contract(name = :default): used to validate the input parameters, typically provided by a user calling an endpoint. A special embedded Contract class has to be defined to holds the validations. If the validations fail, the step will fail. Otherwise, the resulting contract will be available in context.

    +
  • +

    step(name): used to run small snippets of arbitrary code. The step doesn’t care about its return value, so to mark the service as failed, #fail! has to be called explicitly.

    +
  • +

    transaction: used to wrap other steps inside a DB transaction.

    +
+ +

The methods defined on the service are automatically provided with the whole context passed as keyword arguments. This allows to define in a very explicit way what dependencies are used by the method. If for whatever reason a key isn’t found in the current context, then Ruby will raise an exception when the method is called.

+ +

Regarding contract classes, they have automatically ActiveModel modules included so all the ActiveModel API is available.

+ + +
+
+
+ +
+

Examples:

+ + +

+

An example from the TrashChannel service

+

+ +
class TrashChannel
+  include Base
+
+  model :channel, :fetch_channel
+  policy :invalid_access
+  transaction do
+    step :prevents_slug_collision
+    step :soft_delete_channel
+    step :log_channel_deletion
+  end
+  step :enqueue_delete_channel_relations_job
+
+  private
+
+  def fetch_channel(channel_id:, **)
+    ChatChannel.find_by(id: channel_id)
+  end
+
+  def invalid_access(guardian:, channel:, **)
+    guardian.can_preview_chat_channel?(channel) && guardian.can_delete_chat_channel?
+  end
+
+  def prevents_slug_collision(channel:, **)
+    
+  end
+
+  def soft_delete_channel(guardian:, channel:, **)
+    
+  end
+
+  def log_channel_deletion(guardian:, channel:, **)
+    
+  end
+
+  def enqueue_delete_channel_relations_job(channel:, **)
+    
+  end
+end
+ + +

+

An example from the UpdateChannelStatus service which uses a contract

+

+ +
class UpdateChannelStatus
+  include Base
+
+  model :channel, :fetch_channel
+  contract
+  policy :check_channel_permission
+  step :change_status
+
+  class Contract
+    attribute :status
+    validates :status, inclusion: { in: ChatChannel.editable_statuses.keys }
+  end
+
+  
+end
+ +
+ + +

Defined Under Namespace

+

+ + + + + Classes: Context, Failure + + +

+ + + + + + + + +

+ Class Method Summary + collapse +

+ + + + + + + +
+

Class Method Details

+ + +
+

+ + .contract(name = :default, class_name: self::Contract, use_default_values_from: nil) ⇒ Object + + + + + +

+
+ +

Checks the validity of the input parameters. Implements ActiveModel::Validations and ActiveModel::Attributes.

+ +

It stores the resulting contract in context [“contract.default”] by default (can be customized by providing the name argument).

+ + +
+
+
+ +
+

Examples:

+ + +
contract
+
+class Contract
+  attribute :name
+  validates :name, presence: true
+end
+ +
+

Parameters:

+
    + +
  • + + name + + + (Symbol) + + + (defaults to: :default) + + + — +
    +

    name for this contract

    +
    + +
  • + +
  • + + class_name + + + (Class) + + + (defaults to: self::Contract) + + + — +
    +

    a class defining the contract

    +
    + +
  • + +
  • + + use_default_values_from + + + (Symbol) + + + (defaults to: nil) + + + — +
    +

    name of the model to get default values from

    +
    + +
  • + +
+ + +
+
+ +
+

+ + .model(name = :model, step_name = :"fetch_#{name}") ⇒ Object + + + + + +

+
+ +

Evaluates arbitrary code to build or fetch a model (typically from the DB). If the step returns a falsy value, then the step will fail.

+ +

It stores the resulting model in context[:model] by default (can be customized by providing the name argument).

+ + +
+
+
+ +
+

Examples:

+ + +
model :channel, :fetch_channel
+
+private
+
+def fetch_channel(channel_id:, **)
+  ChatChannel.find_by(id: channel_id)
+end
+ +
+

Parameters:

+
    + +
  • + + name + + + (Symbol) + + + (defaults to: :model) + + + — +
    +

    name of the model

    +
    + +
  • + +
  • + + step_name + + + (Symbol) + + + (defaults to: :"fetch_#{name}") + + + — +
    +

    name of the method to call for this step

    +
    + +
  • + +
+ + +
+
+ +
+

+ + .policy(name = :default) ⇒ Object + + + + + +

+
+ +

Performs checks related to the state of the system. If the step doesn’t return a truthy value, then the policy will fail.

+ + +
+
+
+ +
+

Examples:

+ + +
policy :no_direct_message_channel
+
+private
+
+def no_direct_message_channel(channel:, **)
+  !channel.direct_message_channel?
+end
+ +
+

Parameters:

+
    + +
  • + + name + + + (Symbol) + + + (defaults to: :default) + + + — +
    +

    name for this policy

    +
    + +
  • + +
+ + +
+
+ +
+

+ + .step(name) ⇒ Object + + + + + +

+
+ +

Runs arbitrary code. To mark a step as failed, a call to #fail! needs to be made explicitly.

+ + +
+
+
+ +
+

Examples:

+ + +
step :update_channel
+
+private
+
+def update_channel(channel:, params_to_edit:, **)
+  channel.update!(params_to_edit)
+end
+ + +

+

using #fail! in a step

+

+ +
step :save_channel
+
+private
+
+def save_channel(channel:, **)
+  fail!("something went wrong") unless channel.save
+end
+ +
+

Parameters:

+
    + +
  • + + name + + + (Symbol) + + + + — +
    +

    the name of this step

    +
    + +
  • + +
+ + +
+
+ +
+

+ + .transaction(&block) ⇒ Object + + + + + +

+
+ +

Runs steps inside a DB transaction.

+ + +
+
+
+ +
+

Examples:

+ + +
transaction do
+  step :prevents_slug_collision
+  step :soft_delete_channel
+  step :log_channel_deletion
+end
+ +
+

Parameters:

+
    + +
  • + + block + + + (Proc) + + + + — +
    +

    a block containing steps to be run inside a transaction

    +
    + +
  • + +
+ + +
+
+ +
+ +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/backend/Chat/Service/Base/Context.html b/documentation/chat/backend/Chat/Service/Base/Context.html new file mode 100644 index 00000000000..a0031bd16ec --- /dev/null +++ b/documentation/chat/backend/Chat/Service/Base/Context.html @@ -0,0 +1,485 @@ + + + + + + + Class: Chat::Service::Base::Context + + — Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Chat::Service::Base::Context + + + +

+
+ +
+
Inherits:
+
+ OpenStruct + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
plugins/chat/app/services/base.rb
+
+ +
+ +

Overview

+
+ +

Simple structure to hold the context of the service during its whole lifecycle.

+ + +
+
+
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + + +
+

Instance Method Details

+ + +
+

+ + #fail(context = {}) ⇒ Context + + + + + +

+
+ +

Marks the context as failed without raising an exception.

+ + +
+
+
+ +
+

Examples:

+ + +
context.fail("failure": "something went wrong")
+ +
+

Parameters:

+
    + +
  • + + context + + + (Hash, Context) + + + (defaults to: {}) + + + — +
    +

    the context to merge into the current one

    +
    + +
  • + +
+ +

Returns:

+ + +
+
+ +
+

+ + #fail!(context = {}) ⇒ Context + + + + + +

+
+ +

Marks the context as failed.

+ + +
+
+
+ +
+

Examples:

+ + +
context.fail!("failure": "something went wrong")
+ +
+

Parameters:

+
    + +
  • + + context + + + (Hash, Context) + + + (defaults to: {}) + + + — +
    +

    the context to merge into the current one

    +
    + +
  • + +
+ +

Returns:

+ +

Raises:

+ + +
+
+ +
+

+ + #failure?Boolean + + + + + +

+
+ +

Returns true if the context is set as failed

+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + + — +
    +

    returns true if the context is set as failed

    +
    + +
  • + +
+ +

See Also:

+ + +
+
+ +
+

+ + #success?Boolean + + + + + +

+
+ +

Returns true if the conext is set as successful (default)

+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + + — +
    +

    returns true if the conext is set as successful (default)

    +
    + +
  • + +
+ +
+
+ +
+ +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/backend/Chat/Service/Base/Failure.html b/documentation/chat/backend/Chat/Service/Base/Failure.html new file mode 100644 index 00000000000..5f9e93733bf --- /dev/null +++ b/documentation/chat/backend/Chat/Service/Base/Failure.html @@ -0,0 +1,209 @@ + + + + + + + Exception: Chat::Service::Base::Failure + + — Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Exception: Chat::Service::Base::Failure + + + +

+
+ +
+
Inherits:
+
+ StandardError + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
plugins/chat/app/services/base.rb
+
+ +
+ +

Overview

+
+ +

The only exception that can be raised by a service.

+ + +
+
+
+ + +
+ + + +

Instance Attribute Summary collapse

+ + + + + + + +
+

Instance Attribute Details

+ + + +
+

+ + #contextContext (readonly) + + + + + +

+
+ + +
+
+
+ +

Returns:

+ + +
+
+ +
+ + +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/backend/Chat/Service/TrashChannel.html b/documentation/chat/backend/Chat/Service/TrashChannel.html new file mode 100644 index 00000000000..ff9eb948d03 --- /dev/null +++ b/documentation/chat/backend/Chat/Service/TrashChannel.html @@ -0,0 +1,277 @@ + + + + + + + Class: Chat::Service::TrashChannel + + — Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Chat::Service::TrashChannel + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + +
+
Includes:
+
Base
+
+ + + + + + +
+
Defined in:
+
plugins/chat/app/services/trash_channel.rb
+
+ +
+ +

Overview

+
+ +

Service responsible for trashing a chat channel. Note the slug is modified to prevent collisions.

+ + +
+
+
+ +
+

Examples:

+ + +
Chat::Service::TrashChannel.call(channel_id: 2, guardian: guardian)
+ +
+ + +
+ +

+ Constant Summary + collapse +

+ +
+ +
DELETE_CHANNEL_LOG_KEY = + +
+
"chat_channel_delete"
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + + + + + + + + +

Methods included from Base

+

contract, model, policy, step, transaction

+ + + +
+

Instance Method Details

+ + +
+

+ + #call(channel_id: , guardian: ) ⇒ Chat::Service::Base::Context + + + + + +

+
+ + +
+
+
+

Parameters:

+
    + +
  • + + channel_id + + + (Integer) + + + (defaults to: ) + + +
  • + +
  • + + guardian + + + (Guardian) + + + (defaults to: ) + + +
  • + +
+ +

Returns:

+ + +
+
+ +
+ +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/backend/Chat/Service/UpdateChannel.html b/documentation/chat/backend/Chat/Service/UpdateChannel.html new file mode 100644 index 00000000000..166888264f1 --- /dev/null +++ b/documentation/chat/backend/Chat/Service/UpdateChannel.html @@ -0,0 +1,346 @@ + + + + + + + Class: Chat::Service::UpdateChannel + + — Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Chat::Service::UpdateChannel + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + +
+
Includes:
+
Base
+
+ + + + + + +
+
Defined in:
+
plugins/chat/app/services/update_channel.rb
+
+ +
+ +

Overview

+
+ +

Service responsible for updating a chat channel’s name, slug, and description.

+ +

For a CategoryChannel, the settings for auto_join_users and allow_channel_wide_mentions are also editable.

+ + +
+
+
+ +
+

Examples:

+ + +
Chat::Service::UpdateChannel.call(
+ channel_id: 2,
+ guardian: guardian,
+ name: "SuperChannel",
+ description: "This is the best channel",
+ slug: "super-channel",
+)
+ +
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + + + + + + + + +

Methods included from Base

+

contract, model, policy, step, transaction

+ + + +
+

Instance Method Details

+ + +
+

+ + #call(channel_id: , guardian: , **params_to_edit) ⇒ Chat::Service::Base::Context + + + + + +

+
+ + +
+
+
+

Parameters:

+
    + +
  • + + channel_id + + + (Integer) + + + (defaults to: ) + + +
  • + +
  • + + guardian + + + (Guardian) + + + (defaults to: ) + + +
  • + +
  • + + params_to_edit + + + (Hash) + + + +
  • + +
+ + + + + + + + +

Options Hash (**params_to_edit):

+
    + +
  • + name + (String, nil) + + + + +
  • + +
  • + description + (String, nil) + + + + +
  • + +
  • + slug + (String, nil) + + + + +
  • + +
  • + auto_join_users + (Boolean) + + + + + —
    +

    Only valid for CategoryChannel. Whether active users with permission to see the category should automatically join the channel.

    +
    + +
  • + +
  • + allow_channel_wide_mentions + (Boolean) + + + + + —
    +

    Allow the use of @here and @all in the channel.

    +
    + +
  • + +
+ + +

Returns:

+ + +
+
+ +
+ +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/backend/Chat/Service/UpdateChannelStatus.html b/documentation/chat/backend/Chat/Service/UpdateChannelStatus.html new file mode 100644 index 00000000000..85d2adffcf6 --- /dev/null +++ b/documentation/chat/backend/Chat/Service/UpdateChannelStatus.html @@ -0,0 +1,274 @@ + + + + + + + Class: Chat::Service::UpdateChannelStatus + + — Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Chat::Service::UpdateChannelStatus + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + +
+
Includes:
+
Base
+
+ + + + + + +
+
Defined in:
+
plugins/chat/app/services/update_channel_status.rb
+
+ +
+ +

Overview

+
+ +

Service responsible for updating a chat channel status.

+ + +
+
+
+ +
+

Examples:

+ + +
Chat::Service::UpdateChannelStatus.call(channel_id: 2, guardian: guardian, status: "open")
+ +
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + + + + + + + + +

Methods included from Base

+

contract, model, policy, step, transaction

+ + + +
+

Instance Method Details

+ + +
+

+ + #call(channel_id: , guardian: , status: ) ⇒ Chat::Service::Base::Context + + + + + +

+
+ + +
+
+
+

Parameters:

+
    + +
  • + + channel_id + + + (Integer) + + + (defaults to: ) + + +
  • + +
  • + + guardian + + + (Guardian) + + + (defaults to: ) + + +
  • + +
  • + + status + + + (String) + + + (defaults to: ) + + +
  • + +
+ +

Returns:

+ + +
+
+ +
+ +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/backend/Chat/Service/UpdateUserLastRead.html b/documentation/chat/backend/Chat/Service/UpdateUserLastRead.html new file mode 100644 index 00000000000..42533b9c1d5 --- /dev/null +++ b/documentation/chat/backend/Chat/Service/UpdateUserLastRead.html @@ -0,0 +1,274 @@ + + + + + + + Class: Chat::Service::UpdateUserLastRead + + — Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Chat::Service::UpdateUserLastRead + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + +
+
Includes:
+
Base
+
+ + + + + + +
+
Defined in:
+
plugins/chat/app/services/update_user_last_read.rb
+
+ +
+ +

Overview

+
+ +

Service responsible for updating the last read message id of a membership.

+ + +
+
+
+ +
+

Examples:

+ + +
Chat::Service::UpdateUserLastRead.call(user_id: 1, channel_id: 2, guardian: guardian)
+ +
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + + + + + + + + +

Methods included from Base

+

contract, model, policy, step, transaction

+ + + +
+

Instance Method Details

+ + +
+

+ + #call(user_id: , channel_id: , guardian: ) ⇒ Chat::Service::Base::Context + + + + + +

+
+ + +
+
+
+

Parameters:

+
    + +
  • + + user_id + + + (Integer) + + + (defaults to: ) + + +
  • + +
  • + + channel_id + + + (Integer) + + + (defaults to: ) + + +
  • + +
  • + + guardian + + + (Guardian) + + + (defaults to: ) + + +
  • + +
+ +

Returns:

+ + +
+
+ +
+ +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/backend/_index.html b/documentation/chat/backend/_index.html new file mode 100644 index 00000000000..10e4c845e0b --- /dev/null +++ b/documentation/chat/backend/_index.html @@ -0,0 +1,204 @@ + + + + + + + Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Documentation by YARD 0.9.28

+
+

Alphabetic Index

+ +

File Listing

+ + +
+

Namespace Listing A-Z

+ + + + + + + + +
+ + +
    +
  • B
  • +
      + +
    • + Base + + (Chat::Service) + +
    • + +
    +
+ + +
    +
  • C
  • +
      + +
    • + Chat + +
    • + +
    • + Context + + (Chat::Service::Base) + +
    • + +
    +
+ + +
    +
  • F
  • +
      + +
    • + Failure + + (Chat::Service::Base) + +
    • + +
    +
+ + +
    +
  • S
  • + +
+ + + + + + + +
+ +
+ +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/backend/class_list.html b/documentation/chat/backend/class_list.html new file mode 100644 index 00000000000..da9220ff702 --- /dev/null +++ b/documentation/chat/backend/class_list.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + Class List + + + +
+
+

Class List

+ + + +
+ + +
+ + diff --git a/documentation/chat/backend/css/common.css b/documentation/chat/backend/css/common.css new file mode 100644 index 00000000000..cf25c45233d --- /dev/null +++ b/documentation/chat/backend/css/common.css @@ -0,0 +1 @@ +/* Override this file with custom rules */ \ No newline at end of file diff --git a/documentation/chat/backend/css/full_list.css b/documentation/chat/backend/css/full_list.css new file mode 100644 index 00000000000..fa359824291 --- /dev/null +++ b/documentation/chat/backend/css/full_list.css @@ -0,0 +1,58 @@ +body { + margin: 0; + font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; + font-size: 13px; + height: 101%; + overflow-x: hidden; + background: #fafafa; +} + +h1 { padding: 12px 10px; padding-bottom: 0; margin: 0; font-size: 1.4em; } +.clear { clear: both; } +.fixed_header { position: fixed; background: #fff; width: 100%; padding-bottom: 10px; margin-top: 0; top: 0; z-index: 9999; height: 70px; } +#search { position: absolute; right: 5px; top: 9px; padding-left: 24px; } +#content.insearch #search, #content.insearch #noresults { background: url(data:image/gif;base64,R0lGODlhEAAQAPYAAP///wAAAPr6+pKSkoiIiO7u7sjIyNjY2J6engAAAI6OjsbGxjIyMlJSUuzs7KamppSUlPLy8oKCghwcHLKysqSkpJqamvT09Pj4+KioqM7OzkRERAwMDGBgYN7e3ujo6Ly8vCoqKjY2NkZGRtTU1MTExDw8PE5OTj4+PkhISNDQ0MrKylpaWrS0tOrq6nBwcKysrLi4uLq6ul5eXlxcXGJiYoaGhuDg4H5+fvz8/KKiohgYGCwsLFZWVgQEBFBQUMzMzDg4OFhYWBoaGvDw8NbW1pycnOLi4ubm5kBAQKqqqiQkJCAgIK6urnJyckpKSjQ0NGpqatLS0sDAwCYmJnx8fEJCQlRUVAoKCggICLCwsOTk5ExMTPb29ra2tmZmZmhoaNzc3KCgoBISEiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCAAAACwAAAAAEAAQAAAHaIAAgoMgIiYlg4kACxIaACEJCSiKggYMCRselwkpghGJBJEcFgsjJyoAGBmfggcNEx0flBiKDhQFlIoCCA+5lAORFb4AJIihCRbDxQAFChAXw9HSqb60iREZ1omqrIPdJCTe0SWI09GBACH5BAkIAAAALAAAAAAQABAAAAdrgACCgwc0NTeDiYozCQkvOTo9GTmDKy8aFy+NOBA7CTswgywJDTIuEjYFIY0JNYMtKTEFiRU8Pjwygy4ws4owPyCKwsMAJSTEgiQlgsbIAMrO0dKDGMTViREZ14kYGRGK38nHguHEJcvTyIEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDAggPg4iJAAMJCRUAJRIqiRGCBI0WQEEJJkWDERkYAAUKEBc4Po1GiKKJHkJDNEeKig4URLS0ICImJZAkuQAhjSi/wQyNKcGDCyMnk8u5rYrTgqDVghgZlYjcACTA1sslvtHRgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCQARAtOUoQRGRiFD0kJUYWZhUhKT1OLhR8wBaaFBzQ1NwAlkIszCQkvsbOHL7Y4q4IuEjaqq0ZQD5+GEEsJTDCMmIUhtgk1lo6QFUwJVDKLiYJNUd6/hoEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4uen4ICCA+IkIsDCQkVACWmhwSpFqAABQoQF6ALTkWFnYMrVlhWvIKTlSAiJiVVPqlGhJkhqShHV1lCW4cMqSkAR1ofiwsjJyqGgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCSMhREZGIYYGY2ElYebi56fhyWQniSKAKKfpaCLFlAPhl0gXYNGEwkhGYREUywag1wJwSkHNDU3D0kJYIMZQwk8MjPBLx9eXwuETVEyAC/BOKsuEjYFhoEAIfkECQgAAAAsAAAAABAAEAAAB2eAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4ueICImip6CIQkJKJ4kigynKaqKCyMnKqSEK05StgAGQRxPYZaENqccFgIID4KXmQBhXFkzDgOnFYLNgltaSAAEpxa7BQoQF4aBACH5BAkIAAAALAAAAAAQABAAAAdogACCg4SFggJiPUqCJSWGgkZjCUwZACQkgxGEXAmdT4UYGZqCGWQ+IjKGGIUwPzGPhAc0NTewhDOdL7Ykji+dOLuOLhI2BbaFETICx4MlQitdqoUsCQ2vhKGjglNfU0SWmILaj43M5oEAOwAAAAAAAAAAAA==) no-repeat center left; } +#full_list { padding: 0; list-style: none; margin-left: 0; margin-top: 80px; font-size: 1.1em; } +#full_list ul { padding: 0; } +#full_list li { padding: 0; margin: 0; list-style: none; } +#full_list li .item { padding: 5px 5px 5px 12px; } +#noresults { padding: 7px 12px; background: #fff; } +#content.insearch #noresults { margin-left: 7px; } +li.collapsed ul { display: none; } +li a.toggle { cursor: default; position: relative; left: -5px; top: 4px; text-indent: -999px; width: 10px; height: 9px; margin-left: -10px; display: block; float: left; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTM5jWRgMAAAAVdEVYdENyZWF0aW9uIFRpbWUAMy8xNC8wOeNZPpQAAAE2SURBVDiNrZTBccIwEEXfelIAHUA6CZ24BGaWO+FuzZAK4k6gg5QAdGAq+Bxs2Yqx7BzyL7Llp/VfzZeQhCTc/ezuGzKKnKSzpCxXJM8fwNXda3df5RZETlIt6YUzSQDs93sl8w3wBZxCCE10GM1OcWbWjB2mWgEH4Mfdyxm3PSepBHibgQE2wLe7r4HjEidpnXMYdQPKEMJcsZ4zs2POYQOcaPfwMVOo58zsAdMt18BuoVDPxUJRacELbXv3hUIX2vYmOUvi8C8ydz/ThjXrqKqqLbDIAdsCKBd+Wo7GWa7o9qzOQHVVVXeAbs+yHHCH4aTsaCOQqunmUy1yBUAXkdMIfMlgF5EXLo2OpV/c/Up7jG4hhHcYLgWzAZXUc2b2ixsfvc/RmNNfOXD3Q/oeL9axJE1yT9IOoUu6MGUkAAAAAElFTkSuQmCC) no-repeat bottom left; } +li.collapsed a.toggle { opacity: 0.5; cursor: default; background-position: top left; } +li { color: #888; cursor: pointer; } +li.deprecated { text-decoration: line-through; font-style: italic; } +li.odd { background: #f0f0f0; } +li.even { background: #fafafa; } +.item:hover { background: #ddd; } +li small:before { content: "("; } +li small:after { content: ")"; } +li small.search_info { display: none; } +a, a:visited { text-decoration: none; color: #05a; } +li.clicked > .item { background: #05a; color: #ccc; } +li.clicked > .item a, li.clicked > .item a:visited { color: #eee; } +li.clicked > .item a.toggle { opacity: 0.5; background-position: bottom right; } +li.collapsed.clicked a.toggle { background-position: top right; } +#search input { border: 1px solid #bbb; border-radius: 3px; } +#full_list_nav { margin-left: 10px; font-size: 0.9em; display: block; color: #aaa; } +#full_list_nav a, #nav a:visited { color: #358; } +#full_list_nav a:hover { background: transparent; color: #5af; } +#full_list_nav span:after { content: ' | '; } +#full_list_nav span:last-child:after { content: ''; } + +#content h1 { margin-top: 0; } +li { white-space: nowrap; cursor: normal; } +li small { display: block; font-size: 0.8em; } +li small:before { content: ""; } +li small:after { content: ""; } +li small.search_info { display: none; } +#search { width: 170px; position: static; margin: 3px; margin-left: 10px; font-size: 0.9em; color: #888; padding-left: 0; padding-right: 24px; } +#content.insearch #search { background-position: center right; } +#search input { width: 110px; } + +#full_list.insearch ul { display: block; } +#full_list.insearch .item { display: none; } +#full_list.insearch .found { display: block; padding-left: 11px !important; } +#full_list.insearch li a.toggle { display: none; } +#full_list.insearch li small.search_info { display: block; } diff --git a/documentation/chat/backend/css/style.css b/documentation/chat/backend/css/style.css new file mode 100644 index 00000000000..eb0dbc86f68 --- /dev/null +++ b/documentation/chat/backend/css/style.css @@ -0,0 +1,497 @@ +html { + width: 100%; + height: 100%; +} +body { + font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; + font-size: 13px; + width: 100%; + margin: 0; + padding: 0; + display: flex; + display: -webkit-flex; + display: -ms-flexbox; +} + +#nav { + position: relative; + width: 100%; + height: 100%; + border: 0; + border-right: 1px dotted #eee; + overflow: auto; +} +.nav_wrap { + margin: 0; + padding: 0; + width: 20%; + height: 100%; + position: relative; + display: flex; + display: -webkit-flex; + display: -ms-flexbox; + flex-shrink: 0; + -webkit-flex-shrink: 0; + -ms-flex: 1 0; +} +#resizer { + position: absolute; + right: -5px; + top: 0; + width: 10px; + height: 100%; + cursor: col-resize; + z-index: 9999; +} +#main { + flex: 5 1; + -webkit-flex: 5 1; + -ms-flex: 5 1; + outline: none; + position: relative; + background: #fff; + padding: 1.2em; + padding-top: 0.2em; + box-sizing: border-box; +} + +@media (max-width: 920px) { + .nav_wrap { width: 100%; top: 0; right: 0; overflow: visible; position: absolute; } + #resizer { display: none; } + #nav { + z-index: 9999; + background: #fff; + display: none; + position: absolute; + top: 40px; + right: 12px; + width: 500px; + max-width: 80%; + height: 80%; + overflow-y: scroll; + border: 1px solid #999; + border-collapse: collapse; + box-shadow: -7px 5px 25px #aaa; + border-radius: 2px; + } +} + +@media (min-width: 920px) { + body { height: 100%; overflow: hidden; } + #main { height: 100%; overflow: auto; } + #search { display: none; } +} + +#main img { max-width: 100%; } +h1 { font-size: 25px; margin: 1em 0 0.5em; padding-top: 4px; border-top: 1px dotted #d5d5d5; } +h1.noborder { border-top: 0px; margin-top: 0; padding-top: 4px; } +h1.title { margin-bottom: 10px; } +h1.alphaindex { margin-top: 0; font-size: 22px; } +h2 { + padding: 0; + padding-bottom: 3px; + border-bottom: 1px #aaa solid; + font-size: 1.4em; + margin: 1.8em 0 0.5em; + position: relative; +} +h2 small { font-weight: normal; font-size: 0.7em; display: inline; position: absolute; right: 0; } +h2 small a { + display: block; + height: 20px; + border: 1px solid #aaa; + border-bottom: 0; + border-top-left-radius: 5px; + background: #f8f8f8; + position: relative; + padding: 2px 7px; +} +.clear { clear: both; } +.inline { display: inline; } +.inline p:first-child { display: inline; } +.docstring, .tags, #filecontents { font-size: 15px; line-height: 1.5145em; } +.docstring p > code, .docstring p > tt, .tags p > code, .tags p > tt { + color: #c7254e; background: #f9f2f4; padding: 2px 4px; font-size: 1em; + border-radius: 4px; +} +.docstring h1, .docstring h2, .docstring h3, .docstring h4 { padding: 0; border: 0; border-bottom: 1px dotted #bbb; } +.docstring h1 { font-size: 1.2em; } +.docstring h2 { font-size: 1.1em; } +.docstring h3, .docstring h4 { font-size: 1em; border-bottom: 0; padding-top: 10px; } +.summary_desc .object_link a, .docstring .object_link a { + font-family: monospace; font-size: 1.05em; + color: #05a; background: #EDF4FA; padding: 2px 4px; font-size: 1em; + border-radius: 4px; +} +.rdoc-term { padding-right: 25px; font-weight: bold; } +.rdoc-list p { margin: 0; padding: 0; margin-bottom: 4px; } +.summary_desc pre.code .object_link a, .docstring pre.code .object_link a { + padding: 0px; background: inherit; color: inherit; border-radius: inherit; +} + +/* style for */ +#filecontents table, .docstring table { border-collapse: collapse; } +#filecontents table th, #filecontents table td, +.docstring table th, .docstring table td { border: 1px solid #ccc; padding: 8px; padding-right: 17px; } +#filecontents table tr:nth-child(odd), +.docstring table tr:nth-child(odd) { background: #eee; } +#filecontents table tr:nth-child(even), +.docstring table tr:nth-child(even) { background: #fff; } +#filecontents table th, .docstring table th { background: #fff; } + +/* style for
a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
"+""+"
",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
t
",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; +f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() +{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/documentation/chat/backend/method_list.html b/documentation/chat/backend/method_list.html new file mode 100644 index 00000000000..16b5d99b647 --- /dev/null +++ b/documentation/chat/backend/method_list.html @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + Method List + + + +
+
+

Method List

+ + + +
+ +
    + + +
  • +
    + contract + Chat::Service::Base +
    +
  • + + +
  • +
    + model + Chat::Service::Base +
    +
  • + + +
  • +
    + policy + Chat::Service::Base +
    +
  • + + +
  • +
    + step + Chat::Service::Base +
    +
  • + + +
  • +
    + transaction + Chat::Service::Base +
    +
  • + + +
  • +
    + #fail + Chat::Service::Base::Context +
    +
  • + + +
  • +
    + #fail! + Chat::Service::Base::Context +
    +
  • + + +
  • +
    + #failure? + Chat::Service::Base::Context +
    +
  • + + +
  • +
    + #success? + Chat::Service::Base::Context +
    +
  • + + +
  • +
    + #context + Chat::Service::Base::Failure +
    +
  • + + +
  • +
    + #call + Chat::Service::TrashChannel +
    +
  • + + +
  • +
    + #call + Chat::Service::UpdateChannel +
    +
  • + + +
  • +
    + #call + Chat::Service::UpdateChannelStatus +
    +
  • + + +
  • +
    + #call + Chat::Service::UpdateUserLastRead +
    +
  • + + + +
+
+ + diff --git a/documentation/chat/backend/top-level-namespace.html b/documentation/chat/backend/top-level-namespace.html new file mode 100644 index 00000000000..5ac499bd4e8 --- /dev/null +++ b/documentation/chat/backend/top-level-namespace.html @@ -0,0 +1,111 @@ + + + + + + + Top Level Namespace + + — Documentation by YARD 0.9.28 + + + + + + + + + + + + + + + + + + + +
+ + +

Top Level Namespace + + + +

+
+ + + + + + + + + + + +
+ +

Defined Under Namespace

+

+ + + Modules: Chat + + + + +

+ + + + + + + + + +
+ + + + +
+ + \ No newline at end of file diff --git a/documentation/chat/frontend/PluginApi.html b/documentation/chat/frontend/PluginApi.html new file mode 100644 index 00000000000..90810a73205 --- /dev/null +++ b/documentation/chat/frontend/PluginApi.html @@ -0,0 +1,1217 @@ + + + + + Discourse: PluginApi + + + + + + + + +
+ +

+ + Discourse + +

+ +
+ + +
+
+

Class

+

PluginApi

+ + + + + +
+ + +
+ + + + + + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + +
+ +
+ +

new PluginApi() +

+ + + + + +
+ + Class exposing the javascript API available to plugins and themes. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +

Source

+ + + + + +
+ + + + + + + + + + + + + + + +

Methods

+ + + + + +
+ + + + + + + + + +
+ +
+ +

decorateChatMessage(decorator) +

+ + + + + +
+ + Decorate a chat message +
+ + + + + + + + + +

Parameters

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +

Example

+ +
api.decorateChatMessage((chatMessage, messageContainer) => {
+  messageContainer.dataset.foo = chatMessage.id;
+});
+ + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

registerChatComposerButton(options) +

+ + + + + +
+ + Register a button in the chat composer +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + options + + + + + + + + Object + + + + + + + + + +
    + +

    Properties

    + + +
      + + +
    • + + id + + + + + + + + number + + + + + + + + + + + + + + + + + +
      The id of the button
      + +
    • + + + +
    • + + action + + + + + + + + function + + + + + + + + + + + + + + + + + +
      An action name or an anonymous function called when the button is pressed, eg: "onFooClicked" or `() => { console.log("clicked") }`
      + +
    • + + + +
    • + + icon + + + + + + + + string + + + + + + + + + + + + + + + + + +
      A valid font awesome icon name, eg: "far fa-image"
      + +
    • + + + +
    • + + label + + + + + + + + string + + + + + + + + + + + + + + + + + +
      Text displayed on the button, a translatable key, eg: "foo.bar"
      + +
    • + + + +
    • + + translatedLabel + + + + + + + + string + + + + + + + + + + + + + + + + + +
      Text displayed on the button, a string, eg: "Add gifs"
      + +
    • + + + +
    • + + position + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Can be "inline" or "dropdown", defaults to "inline"
      + +
    • + + + +
    • + + title + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Title attribute of the button, a translatable key, eg: "foo.bar"
      + +
    • + + + +
    • + + translatedTitle + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Title attribute of the button, a string, eg: "Add gifs"
      + +
    • + + + +
    • + + ariaLabel + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      aria-label attribute of the button, a translatable key, eg: "foo.bar"
      + +
    • + + + +
    • + + translatedAriaLabel + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      aria-label attribute of the button, a string, eg: "Add gifs"
      + +
    • + + + +
    • + + classNames + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Additional names to add to the button’s class attribute, eg: ["foo", "bar"]
      + +
    • + + + +
    • + + displayed + + + + + + + + boolean + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Hide or show the button
      + +
    • + + + +
    • + + disabled + + + + + + + + boolean + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Sets the disabled attribute on the button
      + +
    • + + + +
    • + + priority + + + + + + + + number + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      An integer defining the order of the buttons, higher comes first, eg: `700`
      + +
    • + + + +
    • + + dependentKeys + + + + + + + + Array.<string> + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      List of property names which should trigger a refresh of the buttons when changed, eg: `["foo.bar", "bar.baz"]`
      + +
    • + + +
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +

Example

+ +
api.registerChatComposerButton({
+  id: "foo",
+  displayed() {
+    return this.site.mobileView && this.canAttachUploads;
+  }
+});
+ + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +

Type Definitions

+ + + + + +
+ + + + + + + + + +
+ +
+ +

decorateChatMessageCallback(chatMessage, messageContainer, chatChannel) +

+ + + + + +
+ + Callback used to decorate a chat message +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + chatMessage + + + + + + + + ChatMessage + + + + + + + + + +
    model
    + +
  • + + + +
  • + + messageContainer + + + + + + + + HTMLElement + + + + + + + + + +
    DOM node
    + +
  • + + + +
  • + + chatChannel + + + + + + + + ChatChannel + + + + + + + + + +
    model
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + + + + + + \ No newline at end of file diff --git a/documentation/chat/frontend/global.html b/documentation/chat/frontend/global.html new file mode 100644 index 00000000000..3ac821fd57f --- /dev/null +++ b/documentation/chat/frontend/global.html @@ -0,0 +1,372 @@ + + + + + Discourse: Global + + + + + + + + +
+ +

+ + Discourse + +

+ +
+ + +
+
+

+

Global

+ + + + + +
+ + +
+ + + + + + + + + + + + +
+ +
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + +

Methods

+ + + + + +
+ + + + + + + + + +
+ +
+ +

load() → {Promise} +

+ + + + + +
+ + Loads first batch of results +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

loadMore() → {Promise} +

+ + + + + +
+ + Attempts to load more results +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + + + + + + \ No newline at end of file diff --git a/documentation/chat/frontend/index.html b/documentation/chat/frontend/index.html new file mode 100644 index 00000000000..1aa37512e0f --- /dev/null +++ b/documentation/chat/frontend/index.html @@ -0,0 +1,80 @@ + + + + + Discourse: + + + + + + + + +
+ +

+ + Discourse + +

+ +
+ + +
+
+

+

+ + + + + + + +

+ + + + + + + + + + + + + + + +
+

This plugin is still in active development and may change frequently

+

Documentation

+

The Discourse Chat plugin adds chat functionality to your Discourse so it can natively support both long-form and short-form communication needs of your online community.

+

For user documentation, see Discourse Chat.

+

For developer documentation, see Discourse Documentation.

+
+ + + + + + +
+
+ + + + + + + + \ No newline at end of file diff --git a/documentation/chat/frontend/lib_collection.js.html b/documentation/chat/frontend/lib_collection.js.html new file mode 100644 index 00000000000..53d771795cd --- /dev/null +++ b/documentation/chat/frontend/lib_collection.js.html @@ -0,0 +1,178 @@ + + + + + Discourse: lib/collection.js + + + + + + + + +
+ +

+ + Discourse + +

+ +
+ + +
+
+

source

+

lib/collection.js

+ + + + + + +
+
+
import { ajax } from "discourse/lib/ajax";
+import { tracked } from "@glimmer/tracking";
+import { bind } from "discourse-common/utils/decorators";
+import { Promise } from "rsvp";
+
+/**
+ * Handles a paginated API response.
+ */
+export default class Collection {
+  @tracked items = [];
+  @tracked meta = {};
+  @tracked loading = false;
+
+  constructor(resourceURL, handler) {
+    this._resourceURL = resourceURL;
+    this._handler = handler;
+    this._fetchedAll = false;
+  }
+
+  get loadMoreURL() {
+    return this.meta.load_more_url;
+  }
+
+  get totalRows() {
+    return this.meta.total_rows;
+  }
+
+  get length() {
+    return this.items.length;
+  }
+
+  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
+  [Symbol.iterator]() {
+    let index = 0;
+
+    return {
+      next: () => {
+        if (index < this.items.length) {
+          return { value: this.items[index++], done: false };
+        } else {
+          return { done: true };
+        }
+      },
+    };
+  }
+
+  /**
+   * Loads first batch of results
+   * @returns {Promise}
+   */
+  @bind
+  load(params = {}) {
+    this._fetchedAll = false;
+
+    if (this.loading) {
+      return Promise.resolve();
+    }
+
+    this.loading = true;
+
+    const filteredQueryParams = Object.entries(params).filter(
+      ([, v]) => v !== undefined
+    );
+    const queryString = new URLSearchParams(filteredQueryParams).toString();
+
+    const endpoint = this._resourceURL + (queryString ? `?${queryString}` : "");
+    return this.#fetch(endpoint)
+      .then((result) => {
+        this.items = this._handler(result);
+        this.meta = result.meta;
+      })
+      .finally(() => {
+        this.loading = false;
+      });
+  }
+
+  /**
+   * Attempts to load more results
+   * @returns {Promise}
+   */
+  @bind
+  loadMore() {
+    let promise = Promise.resolve();
+
+    if (this.loading) {
+      return promise;
+    }
+
+    if (
+      this._fetchedAll ||
+      (this.totalRows && this.items.length >= this.totalRows)
+    ) {
+      return promise;
+    }
+
+    this.loading = true;
+
+    if (this.loadMoreURL) {
+      promise = this.#fetch(this.loadMoreURL).then((result) => {
+        const newItems = this._handler(result);
+
+        if (newItems.length) {
+          this.items = this.items.concat(newItems);
+        } else {
+          this._fetchedAll = true;
+        }
+        this.meta = result.meta;
+      });
+    }
+
+    return promise.finally(() => {
+      this.loading = false;
+    });
+  }
+
+  #fetch(url) {
+    return ajax(url, { type: "GET" });
+  }
+}
+
+
+
+ + + + +
+
+ + + + + + + + diff --git a/documentation/chat/frontend/module-ChatApi.html b/documentation/chat/frontend/module-ChatApi.html new file mode 100644 index 00000000000..fc73b931c23 --- /dev/null +++ b/documentation/chat/frontend/module-ChatApi.html @@ -0,0 +1,3236 @@ + + + + + Discourse: ChatApi + + + + + + + + +
+ +

+ + Discourse + +

+ +
+ + +
+
+

Module

+

ChatApi

+ + + + + +
+ + +
+ + + +
+ +
+
+ + + + + +
Chat API service. Provides methods to interact with the chat API.
+ + + + + +
+ + + + + + + + + + + +

Implements

+
    + +
  • {@ember/service}
  • + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + +

Methods

+ + + + + +
+ + + + + + + + + +
+ +
+ +

categoryPermissions(categoryId) → {Promise} +

+ + + + + +
+ + Lists chat permissions for a category. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + categoryId + + + + + + + + number + + + + + + + + + +
    ID of the category.
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

channel(channelId) → {Promise} +

+ + + + + +
+ + Get a channel by its ID. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    The ID of the channel.
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + +

Example

+ +
this.chatApi.channel(1).then(channel => { ... })
+ + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

channels() → {Collection} +

+ + + + + +
+ + List all accessible category channels of the current user. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Collection + + +
  • + +
+ + + + +

Example

+ +
this.chatApi.channels.then(channels => { ... })
+ + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

createChannel(data) → {Promise} +

+ + + + + +
+ + Creates a channel. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + data + + + + + + + + object + + + + + + + + + +
    Params of the channel.
    + +

    Properties

    + + +
      + + +
    • + + name + + + + + + + + string + + + + + + + + + + + + + + + + + +
      The name of the channel.
      + +
    • + + + +
    • + + chatable_id + + + + + + + + string + + + + + + + + + + + + + + + + + +
      The category of the channel.
      + +
    • + + + +
    • + + description + + + + + + + + string + + + + + + + + + + + + + + + + + +
      The description of the channel.
      + +
    • + + + +
    • + + auto_join_users + + + + + + + + boolean + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Should users join this channel automatically.
      + +
    • + + +
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + +

Example

+ +
this.chatApi
+     .createChannel({ name: "foo", chatable_id: 1, description "bar" })
+     .then((channel) => { ... })
+ + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

createChannelArchive(channelId, data) → {Promise} +

+ + + + + +
+ + Creates a channel archive. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    The ID of the channel.
    + +
  • + + + +
  • + + data + + + + + + + + object + + + + + + + + + +
    Params of the archive.
    + +

    Properties

    + + +
      + + +
    • + + selection + + + + + + + + string + + + + + + + + + + + + + + + + + +
      "new_topic" or "existing_topic".
      + +
    • + + + +
    • + + title + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Title of the topic when creating a new topic.
      + +
    • + + + +
    • + + category_id + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      ID of the category used when creating a new topic.
      + +
    • + + + +
    • + + tags + + + + + + + + Array.<string> + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      tags used when creating a new topic.
      + +
    • + + + +
    • + + topic_id + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      ID of the topic when using an existing topic.
      + +
    • + + +
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

destroyChannel(channelId) → {Promise} +

+ + + + + +
+ + Destroys a channel. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    The ID of the channel.
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + +

Example

+ +
this.chatApi.destroyChannel(1).then(() => { ... })
+ + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

followChannel(channelId) → {Promise} +

+ + + + + +
+ + Makes current user follow a channel. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    The ID of the channel.
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

listChannelMemberships(channelId) → {Collection} +

+ + + + + +
+ + Lists members of a channel. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    The ID of the channel.
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Collection + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

listCurrentUserChannels() → {Promise} +

+ + + + + +
+ + Lists public and direct message channels of the current user. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

moveChannelMessages(channelId, data) → {Promise} +

+ + + + + +
+ + Moves messages from one channel to another. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    The ID of the original channel.
    + +
  • + + + +
  • + + data + + + + + + + + object + + + + + + + + + +
    Params of the move.
    + +

    Properties

    + + +
      + + +
    • + + message_ids + + + + + + + + Array.<number> + + + + + + + + + +
      IDs of the moved messages.
      + +
    • + + + +
    • + + destination_channel_id + + + + + + + + number + + + + + + + + + +
      ID of the channel where the messages are moved to.
      + +
    • + + +
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + +

Example

+ +
this.chatApi
+    .moveChannelMessages(1, {
+      message_ids: [2, 3],
+      destination_channel_id: 4,
+    }).then(() => { ... })
+ + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

sendMessage(channelId, data) → {Promise} +

+ + + + + +
+ + Sends a message. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    ID of the channel.
    + +
  • + + + +
  • + + data + + + + + + + + object + + + + + + + + + +
    Params of the message.
    + +

    Properties

    + + +
      + + +
    • + + message + + + + + + + + string + + + + + + + + + + + + + + + + + +
      The raw content of the message in markdown.
      + +
    • + + + +
    • + + cooked + + + + + + + + string + + + + + + + + + + + + + + + + + +
      The cooked content of the message.
      + +
    • + + + +
    • + + in_reply_to_id + + + + + + + + number + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      The ID of the replied-to message.
      + +
    • + + + +
    • + + staged_id + + + + + + + + number + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      The staged ID of the message before it was persisted.
      + +
    • + + + +
    • + + upload_ids + + + + + + + + Array.<number> + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Array of upload ids linked to the message.
      + +
    • + + +
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

unfollowChannel(channelId) → {Promise} +

+ + + + + +
+ + Makes current user unfollow a channel. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    The ID of the channel.
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

updateChannel(channelId, data) → {Promise} +

+ + + + + +
+ + Updates a channel. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    The ID of the channel.
    + +
  • + + + +
  • + + data + + + + + + + + object + + + + + + + + + +
    Params of the archive.
    + +

    Properties

    + + +
      + + +
    • + + description + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Description of the channel.
      + +
    • + + + +
    • + + name + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Name of the channel.
      + +
    • + + +
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

updateChannelStatus(channelId, status) → {Promise} +

+ + + + + +
+ + Updates the status of a channel. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    The ID of the channel.
    + +
  • + + + +
  • + + status + + + + + + + + string + + + + + + + + + +
    The new status, can be "open" or "closed".
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ +

updateCurrentUserChannelNotificationsSettings(channelId, data) → {Promise} +

+ + + + + +
+ + Update notifications settings of current user for a channel. +
+ + + + + + + + + +

Parameters

+ + +
    + + +
  • + + channelId + + + + + + + + number + + + + + + + + + +
    The ID of the channel.
    + +
  • + + + +
  • + + data + + + + + + + + object + + + + + + + + + +
    The settings to modify.
    + +

    Properties

    + + +
      + + +
    • + + muted + + + + + + + + boolean + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Mutes the channel.
      + +
    • + + + +
    • + + desktop_notification_level + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Notifications level on desktop: never, mention or always.
      + +
    • + + + +
    • + + mobile_notification_level + + + + + + + + string + + + + + + + + + <optional>
      + + + + + +
      + + + + +
      Notifications level on mobile: never, mention or always.
      + +
    • + + +
    + +
  • + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

Returns

+
    + +
  • + + Promise + + +
  • + +
+ + + + + + + + + + + + + + + + +

Source

+ + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + + + + + + \ No newline at end of file diff --git a/documentation/chat/frontend/module.exports.html b/documentation/chat/frontend/module.exports.html new file mode 100644 index 00000000000..fb56f529cb8 --- /dev/null +++ b/documentation/chat/frontend/module.exports.html @@ -0,0 +1,198 @@ + + + + + Discourse: exports + + + + + + + + +
+ +

+ + Discourse + +

+ +
+ + +
+
+

Class

+

exports

+ + + + + +
+ + +
+ + + + + + + + + + + + +
Handles a paginated API response.
+ + +
+ +
+
+ + + + + + + + + + + + + + +

Constructor

+ + +
+ +
+ +

new exports() +

+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +

Source

+ + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+
+ + + + + + + + \ No newline at end of file diff --git a/documentation/chat/frontend/pre-initializers_chat-plugin-api.js.html b/documentation/chat/frontend/pre-initializers_chat-plugin-api.js.html new file mode 100644 index 00000000000..d78c363d0a4 --- /dev/null +++ b/documentation/chat/frontend/pre-initializers_chat-plugin-api.js.html @@ -0,0 +1,156 @@ + + + + + Discourse: pre-initializers/chat-plugin-api.js + + + + + + + + +
+ +

+ + Discourse + +

+ +
+ + +
+
+

source

+

pre-initializers/chat-plugin-api.js

+ + + + + + +
+
+
import { withPluginApi } from "discourse/lib/plugin-api";
+import {
+  addChatMessageDecorator,
+  resetChatMessageDecorators,
+} from "discourse/plugins/chat/discourse/components/chat-message";
+import { registerChatComposerButton } from "discourse/plugins/chat/discourse/lib/chat-composer-buttons";
+
+/**
+ * Class exposing the javascript API available to plugins and themes.
+ * @class PluginApi
+ */
+
+/**
+ * Callback used to decorate a chat message
+ *
+ * @callback PluginApi~decorateChatMessageCallback
+ * @param {ChatMessage} chatMessage - model
+ * @param {HTMLElement} messageContainer - DOM node
+ * @param {ChatChannel} chatChannel - model
+ */
+
+/**
+ * Decorate a chat message
+ *
+ * @memberof PluginApi
+ * @instance
+ * @function decorateChatMessage
+ * @param {PluginApi~decorateChatMessageCallback} decorator
+ * @example
+ *
+ * api.decorateChatMessage((chatMessage, messageContainer) => {
+ *   messageContainer.dataset.foo = chatMessage.id;
+ * });
+ */
+
+/**
+ * Register a button in the chat composer
+ *
+ * @memberof PluginApi
+ * @instance
+ * @function registerChatComposerButton
+ * @param {Object} options
+ * @param {number} options.id - The id of the button
+ * @param {function} options.action - An action name or an anonymous function called when the button is pressed, eg: "onFooClicked" or `() => { console.log("clicked") }`
+ * @param {string} options.icon - A valid font awesome icon name, eg: "far fa-image"
+ * @param {string} options.label - Text displayed on the button, a translatable key, eg: "foo.bar"
+ * @param {string} options.translatedLabel - Text displayed on the button, a string, eg: "Add gifs"
+ * @param {string} [options.position] - Can be "inline" or "dropdown", defaults to "inline"
+ * @param {string} [options.title] - Title attribute of the button, a translatable key, eg: "foo.bar"
+ * @param {string} [options.translatedTitle] - Title attribute of the button, a string, eg: "Add gifs"
+ * @param {string} [options.ariaLabel] - aria-label attribute of the button, a translatable key, eg: "foo.bar"
+ * @param {string} [options.translatedAriaLabel] - aria-label attribute of the button, a string, eg: "Add gifs"
+ * @param {string} [options.classNames] - Additional names to add to the button’s class attribute, eg: ["foo", "bar"]
+ * @param {boolean} [options.displayed] - Hide or show the button
+ * @param {boolean} [options.disabled] - Sets the disabled attribute on the button
+ * @param {number} [options.priority] - An integer defining the order of the buttons, higher comes first, eg: `700`
+ * @param {Array.<string>} [options.dependentKeys] - List of property names which should trigger a refresh of the buttons when changed, eg: `["foo.bar", "bar.baz"]`
+ * @example
+ *
+ * api.registerChatComposerButton({
+ *   id: "foo",
+ *   displayed() {
+ *     return this.site.mobileView && this.canAttachUploads;
+ *   }
+ * });
+ */
+
+export default {
+  name: "chat-plugin-api",
+  after: "inject-discourse-objects",
+
+  initialize() {
+    withPluginApi("1.2.0", (api) => {
+      const apiPrototype = Object.getPrototypeOf(api);
+
+      if (!apiPrototype.hasOwnProperty("decorateChatMessage")) {
+        Object.defineProperty(apiPrototype, "decorateChatMessage", {
+          value(decorator) {
+            addChatMessageDecorator(decorator);
+          },
+        });
+      }
+
+      if (!apiPrototype.hasOwnProperty("registerChatComposerButton")) {
+        Object.defineProperty(apiPrototype, "registerChatComposerButton", {
+          value(button) {
+            registerChatComposerButton(button);
+          },
+        });
+      }
+    });
+  },
+
+  teardown() {
+    resetChatMessageDecorators();
+  },
+};
+
+
+
+ + + + +
+
+ + + + + + + + diff --git a/documentation/chat/frontend/scripts/prism-linenumbers.js b/documentation/chat/frontend/scripts/prism-linenumbers.js new file mode 100644 index 00000000000..67ede74adfc --- /dev/null +++ b/documentation/chat/frontend/scripts/prism-linenumbers.js @@ -0,0 +1,57 @@ +(function() { + +if (typeof self === 'undefined' || !self.Prism || !self.document) { + return; +} + +Prism.hooks.add('complete', function (env) { + if (!env.code) { + return; + } + + // works only for wrapped inside
 (not inline)
+  var pre = env.element.parentNode;
+  var clsReg = /\s*\bline-numbers\b\s*/;
+  if (
+    !pre || !/pre/i.test(pre.nodeName) ||
+      // Abort only if nor the 
 nor the  have the class
+    (!clsReg.test(pre.className) && !clsReg.test(env.element.className))
+  ) {
+    return;
+  }
+
+  if (env.element.querySelector(".line-numbers-rows")) {
+    // Abort if line numbers already exists
+    return;
+  }
+
+  if (clsReg.test(env.element.className)) {
+    // Remove the class "line-numbers" from the 
+    env.element.className = env.element.className.replace(clsReg, '');
+  }
+  if (!clsReg.test(pre.className)) {
+    // Add the class "line-numbers" to the 
+    pre.className += ' line-numbers';
+  }
+
+  var match = env.code.match(/\n(?!$)/g);
+  var linesNum = match ? match.length + 1 : 1;
+  var lineNumbersWrapper;
+
+  var lines = new Array(linesNum + 1);
+  lines = lines.join('');
+
+  lineNumbersWrapper = document.createElement('span');
+  lineNumbersWrapper.setAttribute('aria-hidden', 'true');
+  lineNumbersWrapper.className = 'line-numbers-rows';
+  lineNumbersWrapper.innerHTML = lines;
+
+  if (pre.hasAttribute('data-start')) {
+    pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1);
+  }
+
+  env.element.appendChild(lineNumbersWrapper);
+
+});
+
+}());
\ No newline at end of file
diff --git a/documentation/chat/frontend/scripts/prism.dev.js b/documentation/chat/frontend/scripts/prism.dev.js
new file mode 100644
index 00000000000..beb5e5827f7
--- /dev/null
+++ b/documentation/chat/frontend/scripts/prism.dev.js
@@ -0,0 +1,1115 @@
+/* PrismJS 1.13.0
+http://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+json&plugins=line-highlight+line-numbers */
+var _self = (typeof window !== 'undefined')
+	? window   // if in browser
+	: (
+		(typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)
+		? self // if in worker
+		: {}   // if in node js
+	);
+
+/**
+ * Prism: Lightweight, robust, elegant syntax highlighting
+ * MIT license http://www.opensource.org/licenses/mit-license.php/
+ * @author Lea Verou http://lea.verou.me
+ */
+
+var Prism = (function(){
+
+// Private helper vars
+var lang = /\blang(?:uage)?-(\w+)\b/i;
+var uniqueId = 0;
+
+var _ = _self.Prism = {
+	manual: _self.Prism && _self.Prism.manual,
+	disableWorkerMessageHandler: _self.Prism && _self.Prism.disableWorkerMessageHandler,
+	util: {
+		encode: function (tokens) {
+			if (tokens instanceof Token) {
+				return new Token(tokens.type, _.util.encode(tokens.content), tokens.alias);
+			} else if (_.util.type(tokens) === 'Array') {
+				return tokens.map(_.util.encode);
+			} else {
+				return tokens.replace(/&/g, '&').replace(/ text.length) {
+						// Something went terribly wrong, ABORT, ABORT!
+						return;
+					}
+
+					if (str instanceof Token) {
+						continue;
+					}
+
+					if (greedy && i != strarr.length - 1) {
+						pattern.lastIndex = pos;
+						var match = pattern.exec(text);
+						if (!match) {
+							break;
+						}
+
+						var from = match.index + (lookbehind ? match[1].length : 0),
+						    to = match.index + match[0].length,
+						    k = i,
+						    p = pos;
+
+						for (var len = strarr.length; k < len && (p < to || (!strarr[k].type && !strarr[k - 1].greedy)); ++k) {
+							p += strarr[k].length;
+							// Move the index i to the element in strarr that is closest to from
+							if (from >= p) {
+								++i;
+								pos = p;
+							}
+						}
+
+						// If strarr[i] is a Token, then the match starts inside another Token, which is invalid
+						if (strarr[i] instanceof Token) {
+							continue;
+						}
+
+						// Number of tokens to delete and replace with the new match
+						delNum = k - i;
+						str = text.slice(pos, p);
+						match.index -= pos;
+					} else {
+						pattern.lastIndex = 0;
+
+						var match = pattern.exec(str),
+							delNum = 1;
+					}
+
+					if (!match) {
+						if (oneshot) {
+							break;
+						}
+
+						continue;
+					}
+
+					if(lookbehind) {
+						lookbehindLength = match[1] ? match[1].length : 0;
+					}
+
+					var from = match.index + lookbehindLength,
+					    match = match[0].slice(lookbehindLength),
+					    to = from + match.length,
+					    before = str.slice(0, from),
+					    after = str.slice(to);
+
+					var args = [i, delNum];
+
+					if (before) {
+						++i;
+						pos += before.length;
+						args.push(before);
+					}
+
+					var wrapped = new Token(token, inside? _.tokenize(match, inside) : match, alias, match, greedy);
+
+					args.push(wrapped);
+
+					if (after) {
+						args.push(after);
+					}
+
+					Array.prototype.splice.apply(strarr, args);
+
+					if (delNum != 1)
+						_.matchGrammar(text, strarr, grammar, i, pos, true, token);
+
+					if (oneshot)
+						break;
+				}
+			}
+		}
+	},
+
+	tokenize: function(text, grammar, language) {
+		var strarr = [text];
+
+		var rest = grammar.rest;
+
+		if (rest) {
+			for (var token in rest) {
+				grammar[token] = rest[token];
+			}
+
+			delete grammar.rest;
+		}
+
+		_.matchGrammar(text, strarr, grammar, 0, 0, false);
+
+		return strarr;
+	},
+
+	hooks: {
+		all: {},
+
+		add: function (name, callback) {
+			var hooks = _.hooks.all;
+
+			hooks[name] = hooks[name] || [];
+
+			hooks[name].push(callback);
+		},
+
+		run: function (name, env) {
+			var callbacks = _.hooks.all[name];
+
+			if (!callbacks || !callbacks.length) {
+				return;
+			}
+
+			for (var i=0, callback; callback = callbacks[i++];) {
+				callback(env);
+			}
+		}
+	}
+};
+
+var Token = _.Token = function(type, content, alias, matchedStr, greedy) {
+	this.type = type;
+	this.content = content;
+	this.alias = alias;
+	// Copy of the full string this token was created from
+	this.length = (matchedStr || "").length|0;
+	this.greedy = !!greedy;
+};
+
+Token.stringify = function(o, language, parent) {
+	if (typeof o == 'string') {
+		return o;
+	}
+
+	if (_.util.type(o) === 'Array') {
+		return o.map(function(element) {
+			return Token.stringify(element, language, o);
+		}).join('');
+	}
+
+	var env = {
+		type: o.type,
+		content: Token.stringify(o.content, language, parent),
+		tag: 'span',
+		classes: ['token', o.type],
+		attributes: {},
+		language: language,
+		parent: parent
+	};
+
+	if (o.alias) {
+		var aliases = _.util.type(o.alias) === 'Array' ? o.alias : [o.alias];
+		Array.prototype.push.apply(env.classes, aliases);
+	}
+
+	_.hooks.run('wrap', env);
+
+	var attributes = Object.keys(env.attributes).map(function(name) {
+		return name + '="' + (env.attributes[name] || '').replace(/"/g, '"') + '"';
+	}).join(' ');
+
+	return '<' + env.tag + ' class="' + env.classes.join(' ') + '"' + (attributes ? ' ' + attributes : '') + '>' + env.content + '';
+
+};
+
+if (!_self.document) {
+	if (!_self.addEventListener) {
+		// in Node.js
+		return _self.Prism;
+	}
+
+	if (!_.disableWorkerMessageHandler) {
+		// In worker
+		_self.addEventListener('message', function (evt) {
+			var message = JSON.parse(evt.data),
+				lang = message.language,
+				code = message.code,
+				immediateClose = message.immediateClose;
+
+			_self.postMessage(_.highlight(code, _.languages[lang], lang));
+			if (immediateClose) {
+				_self.close();
+			}
+		}, false);
+	}
+
+	return _self.Prism;
+}
+
+//Get current script and highlight
+var script = document.currentScript || [].slice.call(document.getElementsByTagName("script")).pop();
+
+if (script) {
+	_.filename = script.src;
+
+	if (!_.manual && !script.hasAttribute('data-manual')) {
+		if(document.readyState !== "loading") {
+			if (window.requestAnimationFrame) {
+				window.requestAnimationFrame(_.highlightAll);
+			} else {
+				window.setTimeout(_.highlightAll, 16);
+			}
+		}
+		else {
+			document.addEventListener('DOMContentLoaded', _.highlightAll);
+		}
+	}
+}
+
+return _self.Prism;
+
+})();
+
+if (typeof module !== 'undefined' && module.exports) {
+	module.exports = Prism;
+}
+
+// hack for components to work correctly in node.js
+if (typeof global !== 'undefined') {
+	global.Prism = Prism;
+}
+;
+Prism.languages.markup = {
+	'comment': //,
+	'prolog': /<\?[\s\S]+?\?>/,
+	'doctype': //i,
+	'cdata': //i,
+	'tag': {
+		pattern: /<\/?(?!\d)[^\s>\/=$<%]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+))?)*\s*\/?>/i,
+		greedy: true,
+		inside: {
+			'tag': {
+				pattern: /^<\/?[^\s>\/]+/i,
+				inside: {
+					'punctuation': /^<\/?/,
+					'namespace': /^[^\s>\/:]+:/
+				}
+			},
+			'attr-value': {
+				pattern: /=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+)/i,
+				inside: {
+					'punctuation': [
+						/^=/,
+						{
+							pattern: /(^|[^\\])["']/,
+							lookbehind: true
+						}
+					]
+				}
+			},
+			'punctuation': /\/?>/,
+			'attr-name': {
+				pattern: /[^\s>\/]+/,
+				inside: {
+					'namespace': /^[^\s>\/:]+:/
+				}
+			}
+
+		}
+	},
+	'entity': /&#?[\da-z]{1,8};/i
+};
+
+Prism.languages.markup['tag'].inside['attr-value'].inside['entity'] =
+	Prism.languages.markup['entity'];
+
+// Plugin to make entity title show the real entity, idea by Roman Komarov
+Prism.hooks.add('wrap', function(env) {
+
+	if (env.type === 'entity') {
+		env.attributes['title'] = env.content.replace(/&/, '&');
+	}
+});
+
+Prism.languages.xml = Prism.languages.markup;
+Prism.languages.html = Prism.languages.markup;
+Prism.languages.mathml = Prism.languages.markup;
+Prism.languages.svg = Prism.languages.markup;
+
+Prism.languages.css = {
+	'comment': /\/\*[\s\S]*?\*\//,
+	'atrule': {
+		pattern: /@[\w-]+?.*?(?:;|(?=\s*\{))/i,
+		inside: {
+			'rule': /@[\w-]+/
+			// See rest below
+		}
+	},
+	'url': /url\((?:(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,
+	'selector': /[^{}\s][^{};]*?(?=\s*\{)/,
+	'string': {
+		pattern: /("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
+		greedy: true
+	},
+	'property': /[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,
+	'important': /\B!important\b/i,
+	'function': /[-a-z0-9]+(?=\()/i,
+	'punctuation': /[(){};:]/
+};
+
+Prism.languages.css['atrule'].inside.rest = Prism.languages.css;
+
+if (Prism.languages.markup) {
+	Prism.languages.insertBefore('markup', 'tag', {
+		'style': {
+			pattern: /()[\s\S]*?(?=<\/style>)/i,
+			lookbehind: true,
+			inside: Prism.languages.css,
+			alias: 'language-css',
+			greedy: true
+		}
+	});
+
+	Prism.languages.insertBefore('inside', 'attr-value', {
+		'style-attr': {
+			pattern: /\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,
+			inside: {
+				'attr-name': {
+					pattern: /^\s*style/i,
+					inside: Prism.languages.markup.tag.inside
+				},
+				'punctuation': /^\s*=\s*['"]|['"]\s*$/,
+				'attr-value': {
+					pattern: /.+/i,
+					inside: Prism.languages.css
+				}
+			},
+			alias: 'language-css'
+		}
+	}, Prism.languages.markup.tag);
+};
+Prism.languages.clike = {
+	'comment': [
+		{
+			pattern: /(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,
+			lookbehind: true
+		},
+		{
+			pattern: /(^|[^\\:])\/\/.*/,
+			lookbehind: true,
+			greedy: true
+		}
+	],
+	'string': {
+		pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
+		greedy: true
+	},
+	'class-name': {
+		pattern: /((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,
+		lookbehind: true,
+		inside: {
+			punctuation: /[.\\]/
+		}
+	},
+	'keyword': /\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,
+	'boolean': /\b(?:true|false)\b/,
+	'function': /[a-z0-9_]+(?=\()/i,
+	'number': /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,
+	'operator': /--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,
+	'punctuation': /[{}[\];(),.:]/
+};
+
+Prism.languages.javascript = Prism.languages.extend('clike', {
+	'keyword': /\b(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,
+	'number': /\b(?:0[xX][\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|NaN|Infinity)\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee][+-]?\d+)?/,
+	// Allow for all non-ASCII characters (See http://stackoverflow.com/a/2008444)
+	'function': /[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*\()/i,
+	'operator': /-[-=]?|\+[+=]?|!=?=?|<>?>?=?|=(?:==?|>)?|&[&=]?|\|[|=]?|\*\*?=?|\/=?|~|\^=?|%=?|\?|\.{3}/
+});
+
+Prism.languages.insertBefore('javascript', 'keyword', {
+	'regex': {
+		pattern: /((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(\[[^\]\r\n]+]|\\.|[^/\\\[\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,
+		lookbehind: true,
+		greedy: true
+	},
+	// This must be declared before keyword because we use "function" inside the look-forward
+	'function-variable': {
+		pattern: /[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=\s*(?:function\b|(?:\([^()]*\)|[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/i,
+		alias: 'function'
+	},
+	'constant': /\b[A-Z][A-Z\d_]*\b/
+});
+
+Prism.languages.insertBefore('javascript', 'string', {
+	'template-string': {
+		pattern: /`(?:\\[\s\S]|[^\\`])*`/,
+		greedy: true,
+		inside: {
+			'interpolation': {
+				pattern: /\$\{[^}]+\}/,
+				inside: {
+					'interpolation-punctuation': {
+						pattern: /^\$\{|\}$/,
+						alias: 'punctuation'
+					},
+					rest: Prism.languages.javascript
+				}
+			},
+			'string': /[\s\S]+/
+		}
+	}
+});
+
+if (Prism.languages.markup) {
+	Prism.languages.insertBefore('markup', 'tag', {
+		'script': {
+			pattern: /()[\s\S]*?(?=<\/script>)/i,
+			lookbehind: true,
+			inside: Prism.languages.javascript,
+			alias: 'language-javascript',
+			greedy: true
+		}
+	});
+}
+
+Prism.languages.js = Prism.languages.javascript;
+
+Prism.languages.json = {
+	'property': /"(?:\\.|[^\\"\r\n])*"(?=\s*:)/i,
+	'string': {
+		pattern: /"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,
+		greedy: true
+	},
+	'number': /\b0x[\dA-Fa-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee][+-]?\d+)?/,
+	'punctuation': /[{}[\]);,]/,
+	'operator': /:/g,
+	'boolean': /\b(?:true|false)\b/i,
+	'null': /\bnull\b/i
+};
+
+Prism.languages.jsonp = Prism.languages.json;
+
+(function(){
+
+if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) {
+	return;
+}
+
+function $$(expr, con) {
+	return Array.prototype.slice.call((con || document).querySelectorAll(expr));
+}
+
+function hasClass(element, className) {
+  className = " " + className + " ";
+  return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(className) > -1
+}
+
+// Some browsers round the line-height, others don't.
+// We need to test for it to position the elements properly.
+var isLineHeightRounded = (function() {
+	var res;
+	return function() {
+		if(typeof res === 'undefined') {
+			var d = document.createElement('div');
+			d.style.fontSize = '13px';
+			d.style.lineHeight = '1.5';
+			d.style.padding = 0;
+			d.style.border = 0;
+			d.innerHTML = ' 
 '; + document.body.appendChild(d); + // Browsers that round the line-height should have offsetHeight === 38 + // The others should have 39. + res = d.offsetHeight === 38; + document.body.removeChild(d); + } + return res; + } +}()); + +function highlightLines(pre, lines, classes) { + lines = typeof lines === 'string' ? lines : pre.getAttribute('data-line'); + + var ranges = lines.replace(/\s+/g, '').split(','), + offset = +pre.getAttribute('data-line-offset') || 0; + + var parseMethod = isLineHeightRounded() ? parseInt : parseFloat; + var lineHeight = parseMethod(getComputedStyle(pre).lineHeight); + var hasLineNumbers = hasClass(pre, 'line-numbers'); + + for (var i=0, currentRange; currentRange = ranges[i++];) { + var range = currentRange.split('-'); + + var start = +range[0], + end = +range[1] || start; + + var line = pre.querySelector('.line-highlight[data-range="' + currentRange + '"]') || document.createElement('div'); + + line.setAttribute('aria-hidden', 'true'); + line.setAttribute('data-range', currentRange); + line.className = (classes || '') + ' line-highlight'; + + //if the line-numbers plugin is enabled, then there is no reason for this plugin to display the line numbers + if(hasLineNumbers && Prism.plugins.lineNumbers) { + var startNode = Prism.plugins.lineNumbers.getLine(pre, start); + var endNode = Prism.plugins.lineNumbers.getLine(pre, end); + + if (startNode) { + line.style.top = startNode.offsetTop + 'px'; + } + + if (endNode) { + line.style.height = (endNode.offsetTop - startNode.offsetTop) + endNode.offsetHeight + 'px'; + } + } else { + line.setAttribute('data-start', start); + + if(end > start) { + line.setAttribute('data-end', end); + } + + line.style.top = (start - offset - 1) * lineHeight + 'px'; + + line.textContent = new Array(end - start + 2).join(' \n'); + } + + //allow this to play nicely with the line-numbers plugin + if(hasLineNumbers) { + //need to attack to pre as when line-numbers is enabled, the code tag is relatively which screws up the positioning + pre.appendChild(line); + } else { + (pre.querySelector('code') || pre).appendChild(line); + } + } +} + +function applyHash() { + var hash = location.hash.slice(1); + + // Remove pre-existing temporary lines + $$('.temporary.line-highlight').forEach(function (line) { + line.parentNode.removeChild(line); + }); + + var range = (hash.match(/\.([\d,-]+)$/) || [,''])[1]; + + if (!range || document.getElementById(hash)) { + return; + } + + var id = hash.slice(0, hash.lastIndexOf('.')), + pre = document.getElementById(id); + + if (!pre) { + return; + } + + if (!pre.hasAttribute('data-line')) { + pre.setAttribute('data-line', ''); + } + + highlightLines(pre, range, 'temporary '); + + document.querySelector('.temporary.line-highlight').scrollIntoView(); + + // offset fixed header with buffer + window.scrollBy(0, -100); +} + +var fakeTimer = 0; // Hack to limit the number of times applyHash() runs + +Prism.hooks.add('before-sanity-check', function(env) { + var pre = env.element.parentNode; + var lines = pre && pre.getAttribute('data-line'); + + if (!pre || !lines || !/pre/i.test(pre.nodeName)) { + return; + } + + /* + * Cleanup for other plugins (e.g. autoloader). + * + * Sometimes blocks are highlighted multiple times. It is necessary + * to cleanup any left-over tags, because the whitespace inside of the
+ * tags change the content of the tag. + */ + var num = 0; + $$('.line-highlight', pre).forEach(function (line) { + num += line.textContent.length; + line.parentNode.removeChild(line); + }); + // Remove extra whitespace + if (num && /^( \n)+$/.test(env.code.slice(-num))) { + env.code = env.code.slice(0, -num); + } +}); + +Prism.hooks.add('complete', function completeHook(env) { + var pre = env.element.parentNode; + var lines = pre && pre.getAttribute('data-line'); + + if (!pre || !lines || !/pre/i.test(pre.nodeName)) { + return; + } + + clearTimeout(fakeTimer); + + var hasLineNumbers = Prism.plugins.lineNumbers; + var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers; + + if (hasClass(pre, 'line-numbers') && hasLineNumbers && !isLineNumbersLoaded) { + Prism.hooks.add('line-numbers', completeHook); + } else { + highlightLines(pre, lines); + fakeTimer = setTimeout(applyHash, 1); + } +}); + + window.addEventListener('load', applyHash); + window.addEventListener('hashchange', applyHash); + window.addEventListener('resize', function () { + var preElements = document.querySelectorAll('pre[data-line]'); + Array.prototype.forEach.call(preElements, function (pre) { + highlightLines(pre); + }); + }); + +})(); +(function () { + + if (typeof self === 'undefined' || !self.Prism || !self.document) { + return; + } + + /** + * Plugin name which is used as a class name for
 which is activating the plugin
+	 * @type {String}
+	 */
+	var PLUGIN_NAME = 'line-numbers';
+
+	/**
+	 * Regular expression used for determining line breaks
+	 * @type {RegExp}
+	 */
+	var NEW_LINE_EXP = /\n(?!$)/g;
+
+	/**
+	 * Resizes line numbers spans according to height of line of code
+	 * @param {Element} element 
 element
+	 */
+	var _resizeElement = function (element) {
+		var codeStyles = getStyles(element);
+		var whiteSpace = codeStyles['white-space'];
+
+		if (whiteSpace === 'pre-wrap' || whiteSpace === 'pre-line') {
+			var codeElement = element.querySelector('code');
+			var lineNumbersWrapper = element.querySelector('.line-numbers-rows');
+			var lineNumberSizer = element.querySelector('.line-numbers-sizer');
+			var codeLines = codeElement.textContent.split(NEW_LINE_EXP);
+
+			if (!lineNumberSizer) {
+				lineNumberSizer = document.createElement('span');
+				lineNumberSizer.className = 'line-numbers-sizer';
+
+				codeElement.appendChild(lineNumberSizer);
+			}
+
+			lineNumberSizer.style.display = 'block';
+
+			codeLines.forEach(function (line, lineNumber) {
+				lineNumberSizer.textContent = line || '\n';
+				var lineSize = lineNumberSizer.getBoundingClientRect().height;
+				lineNumbersWrapper.children[lineNumber].style.height = lineSize + 'px';
+			});
+
+			lineNumberSizer.textContent = '';
+			lineNumberSizer.style.display = 'none';
+		}
+	};
+
+	/**
+	 * Returns style declarations for the element
+	 * @param {Element} element
+	 */
+	var getStyles = function (element) {
+		if (!element) {
+			return null;
+		}
+
+		return window.getComputedStyle ? getComputedStyle(element) : (element.currentStyle || null);
+	};
+
+	window.addEventListener('resize', function () {
+		Array.prototype.forEach.call(document.querySelectorAll('pre.' + PLUGIN_NAME), _resizeElement);
+	});
+
+	Prism.hooks.add('complete', function (env) {
+		if (!env.code) {
+			return;
+		}
+
+		// works only for  wrapped inside 
 (not inline)
+		var pre = env.element.parentNode;
+		var clsReg = /\s*\bline-numbers\b\s*/;
+		if (
+			!pre || !/pre/i.test(pre.nodeName) ||
+			// Abort only if nor the 
 nor the  have the class
+			(!clsReg.test(pre.className) && !clsReg.test(env.element.className))
+		) {
+			return;
+		}
+
+		if (env.element.querySelector('.line-numbers-rows')) {
+			// Abort if line numbers already exists
+			return;
+		}
+
+		if (clsReg.test(env.element.className)) {
+			// Remove the class 'line-numbers' from the 
+			env.element.className = env.element.className.replace(clsReg, ' ');
+		}
+		if (!clsReg.test(pre.className)) {
+			// Add the class 'line-numbers' to the 
+			pre.className += ' line-numbers';
+		}
+
+		var match = env.code.match(NEW_LINE_EXP);
+		var linesNum = match ? match.length + 1 : 1;
+		var lineNumbersWrapper;
+
+		var lines = new Array(linesNum + 1);
+		lines = lines.join('');
+
+		lineNumbersWrapper = document.createElement('span');
+		lineNumbersWrapper.setAttribute('aria-hidden', 'true');
+		lineNumbersWrapper.className = 'line-numbers-rows';
+		lineNumbersWrapper.innerHTML = lines;
+
+		if (pre.hasAttribute('data-start')) {
+			pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1);
+		}
+
+		env.element.appendChild(lineNumbersWrapper);
+
+		_resizeElement(pre);
+
+		Prism.hooks.run('line-numbers', env);
+	});
+
+	Prism.hooks.add('line-numbers', function (env) {
+		env.plugins = env.plugins || {};
+		env.plugins.lineNumbers = true;
+	});
+
+	/**
+	 * Global exports
+	 */
+	Prism.plugins.lineNumbers = {
+		/**
+		 * Get node for provided line number
+		 * @param {Element} element pre element
+		 * @param {Number} number line number
+		 * @return {Element|undefined}
+		 */
+		getLine: function (element, number) {
+			if (element.tagName !== 'PRE' || !element.classList.contains(PLUGIN_NAME)) {
+				return;
+			}
+
+			var lineNumberRows = element.querySelector('.line-numbers-rows');
+			var lineNumberStart = parseInt(element.getAttribute('data-start'), 10) || 1;
+			var lineNumberEnd = lineNumberStart + (lineNumberRows.children.length - 1);
+
+			if (number < lineNumberStart) {
+				number = lineNumberStart;
+			}
+			if (number > lineNumberEnd) {
+				number = lineNumberEnd;
+			}
+
+			var lineIndex = number - lineNumberStart;
+
+			return lineNumberRows.children[lineIndex];
+		}
+	};
+
+}());
diff --git a/documentation/chat/frontend/services_chat-api.js.html b/documentation/chat/frontend/services_chat-api.js.html
new file mode 100644
index 00000000000..67456b35620
--- /dev/null
+++ b/documentation/chat/frontend/services_chat-api.js.html
@@ -0,0 +1,325 @@
+
+
+
+    
+    Discourse: services/chat-api.js
+    
+      
+    
+    
+    
+
+
+
+
+ +

+ + Discourse + +

+ +
+ + +
+
+

source

+

services/chat-api.js

+ + + + + + +
+
+
import Service, { inject as service } from "@ember/service";
+import { ajax } from "discourse/lib/ajax";
+import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
+import Collection from "../lib/collection";
+
+/**
+ * Chat API service. Provides methods to interact with the chat API.
+ *
+ * @module ChatApi
+ * @implements {@ember/service}
+ */
+export default class ChatApi extends Service {
+  @service chatChannelsManager;
+
+  /**
+   * Get a channel by its ID.
+   * @param {number} channelId - The ID of the channel.
+   * @returns {Promise}
+   *
+   * @example
+   *
+   *    this.chatApi.channel(1).then(channel => { ... })
+   */
+  channel(channelId) {
+    return this.#getRequest(`/channels/${channelId}`).then((result) =>
+      this.chatChannelsManager.store(result.channel)
+    );
+  }
+
+  /**
+   * List all accessible category channels of the current user.
+   * @returns {Collection}
+   *
+   * @example
+   *
+   *    this.chatApi.channels.then(channels => { ... })
+   */
+  channels() {
+    return new Collection(`${this.#basePath}/channels`, (response) => {
+      return response.channels.map((channel) =>
+        this.chatChannelsManager.store(channel)
+      );
+    });
+  }
+
+  /**
+   * Moves messages from one channel to another.
+   * @param {number} channelId - The ID of the original channel.
+   * @param {object} data - Params of the move.
+   * @param {Array.<number>} data.message_ids - IDs of the moved messages.
+   * @param {number} data.destination_channel_id - ID of the channel where the messages are moved to.
+   * @returns {Promise}
+   *
+   * @example
+   *
+   *   this.chatApi
+   *     .moveChannelMessages(1, {
+   *       message_ids: [2, 3],
+   *       destination_channel_id: 4,
+   *     }).then(() => { ... })
+   */
+  moveChannelMessages(channelId, data = {}) {
+    return this.#postRequest(`/channels/${channelId}/messages/moves`, {
+      move: data,
+    });
+  }
+
+  /**
+   * Destroys a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @returns {Promise}
+   *
+   * @example
+   *
+   *    this.chatApi.destroyChannel(1).then(() => { ... })
+   */
+  destroyChannel(channelId) {
+    return this.#deleteRequest(`/channels/${channelId}`);
+  }
+
+  /**
+   * Creates a channel.
+   * @param {object} data - Params of the channel.
+   * @param {string} data.name - The name of the channel.
+   * @param {string} data.chatable_id - The category of the channel.
+   * @param {string} data.description - The description of the channel.
+   * @param {boolean} [data.auto_join_users] - Should users join this channel automatically.
+   * @returns {Promise}
+   *
+   * @example
+   *
+   *    this.chatApi
+   *      .createChannel({ name: "foo", chatable_id: 1, description "bar" })
+   *      .then((channel) => { ... })
+   */
+  createChannel(data = {}) {
+    return this.#postRequest("/channels", { channel: data }).then((response) =>
+      this.chatChannelsManager.store(response.channel)
+    );
+  }
+
+  /**
+   * Lists chat permissions for a category.
+   * @param {number} categoryId - ID of the category.
+   * @returns {Promise}
+   */
+  categoryPermissions(categoryId) {
+    return this.#getRequest(`/category-chatables/${categoryId}/permissions`);
+  }
+
+  /**
+   * Sends a message.
+   * @param {number} channelId - ID of the channel.
+   * @param {object} data - Params of the message.
+   * @param {string} data.message - The raw content of the message in markdown.
+   * @param {string} data.cooked - The cooked content of the message.
+   * @param {number} [data.in_reply_to_id] - The ID of the replied-to message.
+   * @param {number} [data.staged_id] - The staged ID of the message before it was persisted.
+   * @param {Array.<number>} [data.upload_ids] - Array of upload ids linked to the message.
+   * @returns {Promise}
+   */
+  sendMessage(channelId, data = {}) {
+    return ajax(`/chat/${channelId}`, {
+      ignoreUnsent: false,
+      type: "POST",
+      data,
+    });
+  }
+
+  /**
+   * Creates a channel archive.
+   * @param {number} channelId - The ID of the channel.
+   * @param {object} data - Params of the archive.
+   * @param {string} data.selection - "new_topic" or "existing_topic".
+   * @param {string} [data.title] - Title of the topic when creating a new topic.
+   * @param {string} [data.category_id] - ID of the category used when creating a new topic.
+   * @param {Array.<string>} [data.tags] - tags used when creating a new topic.
+   * @param {string} [data.topic_id] - ID of the topic when using an existing topic.
+   * @returns {Promise}
+   */
+  createChannelArchive(channelId, data = {}) {
+    return this.#postRequest(`/channels/${channelId}/archives`, {
+      archive: data,
+    });
+  }
+
+  /**
+   * Updates a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @param {object} data - Params of the archive.
+   * @param {string} [data.description] - Description of the channel.
+   * @param {string} [data.name] - Name of the channel.
+   * @returns {Promise}
+   */
+  updateChannel(channelId, data = {}) {
+    return this.#putRequest(`/channels/${channelId}`, { channel: data });
+  }
+
+  /**
+   * Updates the status of a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @param {string} status - The new status, can be "open" or "closed".
+   * @returns {Promise}
+   */
+  updateChannelStatus(channelId, status) {
+    return this.#putRequest(`/channels/${channelId}/status`, { status });
+  }
+
+  /**
+   * Lists members of a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @returns {Collection}
+   */
+  listChannelMemberships(channelId) {
+    return new Collection(
+      `${this.#basePath}/channels/${channelId}/memberships`,
+      (response) => {
+        return response.memberships.map((membership) =>
+          UserChatChannelMembership.create(membership)
+        );
+      }
+    );
+  }
+
+  /**
+   * Lists public and direct message channels of the current user.
+   * @returns {Promise}
+   */
+  listCurrentUserChannels() {
+    return this.#getRequest("/channels/me").then((result) => {
+      return (result?.channels || []).map((channel) =>
+        this.chatChannelsManager.store(channel)
+      );
+    });
+  }
+
+  /**
+   * Makes current user follow a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @returns {Promise}
+   */
+  followChannel(channelId) {
+    return this.#postRequest(`/channels/${channelId}/memberships/me`).then(
+      (result) => UserChatChannelMembership.create(result.membership)
+    );
+  }
+
+  /**
+   * Makes current user unfollow a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @returns {Promise}
+   */
+  unfollowChannel(channelId) {
+    return this.#deleteRequest(`/channels/${channelId}/memberships/me`).then(
+      (result) => UserChatChannelMembership.create(result.membership)
+    );
+  }
+
+  /**
+   * Update notifications settings of current user for a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @param {object} data - The settings to modify.
+   * @param {boolean} [data.muted] - Mutes the channel.
+   * @param {string} [data.desktop_notification_level] - Notifications level on desktop: never, mention or always.
+   * @param {string} [data.mobile_notification_level] - Notifications level on mobile: never, mention or always.
+   * @returns {Promise}
+   */
+  updateCurrentUserChannelNotificationsSettings(channelId, data = {}) {
+    return this.#putRequest(
+      `/channels/${channelId}/notifications-settings/me`,
+      { notifications_settings: data }
+    );
+  }
+
+  get #basePath() {
+    return "/chat/api";
+  }
+
+  #getRequest(endpoint, data = {}) {
+    return ajax(`${this.#basePath}${endpoint}`, {
+      type: "GET",
+      data,
+    });
+  }
+
+  #putRequest(endpoint, data = {}) {
+    return ajax(`${this.#basePath}${endpoint}`, {
+      type: "PUT",
+      data,
+    });
+  }
+
+  #postRequest(endpoint, data = {}) {
+    return ajax(`${this.#basePath}${endpoint}`, {
+      type: "POST",
+      data,
+    });
+  }
+
+  #deleteRequest(endpoint, data = {}) {
+    return ajax(`${this.#basePath}${endpoint}`, {
+      type: "DELETE",
+      data,
+    });
+  }
+}
+
+
+
+ + + + +
+
+ + + + + + + + diff --git a/documentation/chat/frontend/styles/styles.css b/documentation/chat/frontend/styles/styles.css new file mode 100644 index 00000000000..159439a937d --- /dev/null +++ b/documentation/chat/frontend/styles/styles.css @@ -0,0 +1,498 @@ +:root { + --primary-color: #0664a8; + --secondary-color: #107e7d; + --link-color: var(--primary-color); + --link-hover-color: var(--primary-color); + --border-color: #eee; + --code-color: #666; + --code-attention-color: #ca2d00; + --text-color: #4a4a4a; + --light-font-color: #999; + --supporting-color: #7097b5; + --heading-color: var(--text-color); + --subheading-color: var(--secondary-color); + --heading-background: #f7f7f7; + --code-bg-color: #f8f8f8; + --nav-title-color: var(--primary-color); + --nav-title-align: center; + --nav-title-size: 1rem; + --nav-title-margin-bottom: 1.5em; + --nav-title-font-weight: 600; + --nav-list-margin-left: 2em; + --nav-bg-color: #fff; + --nav-heading-display: block; + --nav-heading-color: #aaa; + --nav-link-color: #666; + --nav-text-color: #aaa; + --nav-type-class-color: #fff; + --nav-type-class-bg: #FF8C00; + --nav-type-member-color: #39b739; + --nav-type-member-bg: #d5efd5; + --nav-type-function-color: #549ab9; + --nav-type-function-bg: #e1f6ff; + --nav-type-namespace-color: #eb6420; + --nav-type-namespace-bg: #fad8c7; + --nav-type-typedef-color: #964cb1; + --nav-type-typedef-bg: #f2e4f7; + --nav-type-module-color: #964cb1; + --nav-type-module-bg: #f2e4f7; + --nav-type-event-color: #948b34; + --nav-type-event-bg: #fff6a6; + --max-content-width: 900px; + --nav-width: 320px; + --padding-unit: 30px; + --layout-footer-color: #aaa; + --member-name-signature-display: none; + --base-font-size: 16px; + --base-line-height: 1.7; + --body-font: -apple-system, system-ui, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --code-font: Consolas, Monaco, "Andale Mono", monospace; +} + +body { + font-family: var(--body-font); + font-size: var(--base-font-size); + line-height: var(--base-line-height); + color: var(--text-color); + -webkit-font-smoothing: antialiased; + text-size-adjust: 100%; +} + +* { + box-sizing: border-box; +} + +a { + text-decoration: none; + color: var(--link-color); +} +a:hover, a:active { + text-decoration: underline; + color: var(--link-hover-color); +} + +img { + max-width: 100%; +} +img + p { + margin-top: 1em; +} + +ul { + margin: 1em 0; +} + +tt, code, kbd, samp { + font-family: var(--code-font); +} + +code { + display: inline-block; + background-color: var(--code-bg-color); + padding: 2px 6px 0px; + border-radius: 3px; + color: var(--code-attention-color); +} + +.prettyprint.source code:not([class*=language-]) { + display: block; + padding: 20px; + overflow: scroll; + color: var(--code-color); +} + +.layout-main, +.layout-footer { + margin-left: var(--nav-width); +} + +.container { + max-width: var(--max-content-width); + margin-left: auto; + margin-right: auto; +} + +.layout-main { + margin-top: var(--padding-unit); + margin-bottom: var(--padding-unit); + padding: 0 var(--padding-unit); +} + +.layout-header { + background: var(--nav-bg-color); + border-right: 1px solid var(--border-color); + position: fixed; + padding: 0 var(--padding-unit); + top: 0; + left: 0; + right: 0; + width: var(--nav-width); + height: 100%; + overflow: scroll; +} +.layout-header h1 { + display: block; + margin-bottom: var(--nav-title-margin-bottom); + font-size: var(--nav-title-size); + font-weight: var(--nav-title-font-weight); + text-align: var(--nav-title-align); +} +.layout-header h1 a:link, .layout-header h1 a:visited { + color: var(--nav-title-color); +} +.layout-header img { + max-width: 120px; + display: block; + margin: 1em auto; +} + +.layout-nav { + margin-bottom: 2rem; +} +.layout-nav ul { + margin: 0 0 var(--nav-list-margin-left); + padding: 0; +} +.layout-nav li { + list-style-type: none; + font-size: 0.95em; +} +.layout-nav li.nav-heading:first-child { + display: var(--nav-heading-display); + margin-left: 0; + margin-bottom: 1em; + text-transform: uppercase; + color: var(--nav-heading-color); + font-size: 0.85em; +} +.layout-nav a { + color: var(--nav-link-color); +} +.layout-nav a:link, .layout-nav a a:visited { + color: var(--nav-link-color); +} + +.layout-content--source { + max-width: none; +} + +.nav-heading { + margin-top: 1em; + font-weight: 500; +} +.nav-heading a { + color: var(--nav-link-color); +} +.nav-heading a:link, .nav-heading a:visited { + color: var(--nav-link-color); +} +.nav-heading .nav-item-type { + font-size: 0.9em; +} + +.nav-item-type { + display: inline-block; + font-size: 0.9em; + width: 1.2em; + height: 1.2em; + line-height: 1.2em; + display: inline-block; + text-align: center; + border-radius: 0.2em; + margin-right: 0.5em; +} +.nav-item-type.type-class { + color: var(--nav-type-class-color); + background: var(--nav-type-class-bg); +} +.nav-item-type.type-typedef { + color: var(--nav-type-typedef-color); + background: var(--nav-type-typedef-bg); +} +.nav-item-type.type-function { + color: var(--nav-type-function-color); + background: var(--nav-type-function-bg); +} +.nav-item-type.type-namespace { + color: var(--nav-type-namespace-color); + background: var(--nav-type-namespace-bg); +} +.nav-item-type.type-member { + color: var(--nav-type-member-color); + background: var(--nav-type-member-bg); +} +.nav-item-type.type-module { + color: var(--nav-type-module-color); + background: var(--nav-type-module-bg); +} +.nav-item-type.type-event { + color: var(--nav-type-event-color); + background: var(--nav-type-event-bg); +} + +.nav-item-name.is-function:after { + display: inline; + content: "()"; + color: var(--nav-link-color); + opacity: 0.75; +} +.nav-item-name.is-class { + font-size: 1.1em; +} + +.layout-footer { + padding-top: 2rem; + padding-bottom: 2rem; + font-size: 0.8em; + text-align: center; + color: var(--layout-footer-color); +} +.layout-footer a { + color: var(--light-font-color); + text-decoration: underline; +} + +h1 { + font-size: 2rem; + color: var(--heading-color); +} + +h5 { + margin: 0; + font-weight: 500; + font-size: 1em; +} +h5 + .code-caption { + margin-top: 1em; +} + +.page-kind { + margin: 0 0 -0.5em; + font-weight: 400; + color: var(--light-font-color); + text-transform: uppercase; +} + +.page-title { + margin-top: 0; +} + +.subtitle { + font-weight: 600; + font-size: 1.5em; + color: var(--subheading-color); + margin: 1em 0; + padding: 0.4em 0; + border-bottom: 1px solid var(--border-color); +} +.subtitle + .event, .subtitle + .member, .subtitle + .method { + border-top: none; + padding-top: 0; +} + +.method-type + .method-name { + margin-top: 0.5em; +} + +.event-name, +.member-name, +.method-name, +.type-definition-name { + margin: 1em 0; + font-size: 1.4rem; + font-family: var(--code-font); + font-weight: 600; + color: var(--primary-color); +} +.event-name .signature-attributes, +.member-name .signature-attributes, +.method-name .signature-attributes, +.type-definition-name .signature-attributes { + display: inline-block; + margin-left: 0.25em; + font-size: 60%; + color: #999; + font-style: italic; + font-weight: lighter; +} + +.type-signature { + display: inline-block; + margin-left: 0.5em; +} + +.member-name .type-signature { + display: var(--member-name-signature-display); +} + +.type-signature, +.return-type-signature { + color: #aaa; + font-weight: 400; +} +.type-signature a:link, .type-signature a:visited, +.return-type-signature a:link, +.return-type-signature a:visited { + color: #aaa; +} + +table { + margin-top: 1rem; + width: auto; + min-width: 400px; + max-width: 100%; + border-top: 1px solid var(--border-color); + border-right: 1px solid var(--border-color); +} +table th, table h4 { + font-weight: 500; +} +table th, +table td { + padding: 0.5rem 0.75rem; +} +table th, +table td { + border-left: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); +} +table p:last-child { + margin-bottom: 0; +} + +.readme h2 { + border-bottom: 1px solid var(--border-color); + margin: 1em 0; + padding-bottom: 0.5rem; + color: var(--subheading-color); +} +.readme h2 + h3 { + margin-top: 0; +} +.readme h3 { + margin: 2rem 0 1rem 0; +} + +article.event, article.member, article.method { + padding: 1em 0 1em; + margin: 1em 0; + border-top: 1px solid var(--border-color); +} + +.method-type-signature:not(:empty) { + display: inline-block; + background: #ecf0f1; + color: #627475; + padding: 0.25em 0.5em 0.35em; + font-weight: 300; + font-size: 0.8rem; + margin: 0 0.75em 0 0; +} + +.method-heading { + margin: 1em 0; +} + +li.method-returns, +.method-params li { + margin-bottom: 1em; +} + +.method-source a:link, .method-source a:visited { + color: var(--light-font-color); +} + +.method-returns p { + margin: 0; +} + +.event-description, +.method-description { + margin: 0 0 2em; +} + +.param-type code, +.method-returns code { + color: #111; +} + +.param-name { + font-weight: 600; + display: inline-block; + margin-right: 0.5em; +} + +.param-type, +.param-default, +.param-attributes { + font-family: var(--code-font); +} + +.param-default::before { + display: inline-block; + content: "Default:"; + font-family: var(--body-font); +} + +.param-attributes { + color: var(--light-font-color); +} + +.param-description p:first-child { + margin-top: 0; +} + +.param-properties { + font-weight: 500; + margin: 1em 0 0; +} + +.param-types, +.property-types { + display: inline-block; + margin: 0 0.5em 0 0.25em; + color: #999; +} + +.param-attr, +.property-attr { + display: inline-block; + padding: 0.2em 0.5em; + border: 1px solid #eee; + color: #aaa; + font-weight: 300; + font-size: 0.8em; + vertical-align: baseline; +} + +.properties-table p:last-child { + margin-bottom: 0; +} + +pre[class*=language-] { + border-radius: 0; +} + +code[class*=language-], +pre[class*=language-] { + text-shadow: none; + border: none; +} +code[class*=language-].source-page, +pre[class*=language-].source-page { + font-size: 0.9em; +} + +.line-numbers .line-numbers-rows { + border-right: none; +} + +.source-page { + font-size: 14px; +} +.source-page code { + z-index: 1; +} +.source-page .line-height.temporary { + z-index: 0; +} \ No newline at end of file diff --git a/documentation/chat/frontend/styles/vendor/prism-custom.css b/documentation/chat/frontend/styles/vendor/prism-custom.css new file mode 100644 index 00000000000..09d20634024 --- /dev/null +++ b/documentation/chat/frontend/styles/vendor/prism-custom.css @@ -0,0 +1,142 @@ +/* PrismJS 1.17.1 +https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+http */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"] { + color: black; + background: none; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #f6f8fa; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #9a6e3a; + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function, +.token.class-name { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + diff --git a/documentation/index.html b/documentation/index.html new file mode 100644 index 00000000000..83efddc8df2 --- /dev/null +++ b/documentation/index.html @@ -0,0 +1,125 @@ + + + + + + + + + + + Discourse documentation | Discourse - Civilized Discussion + + + + + + +
+
+

Discourse projects documentation

+
+
+ +
+
+ +
+
+ + diff --git a/documentation/yard-custom-template/default/fulldoc/html/setup.rb b/documentation/yard-custom-template/default/fulldoc/html/setup.rb new file mode 100644 index 00000000000..e536537a490 --- /dev/null +++ b/documentation/yard-custom-template/default/fulldoc/html/setup.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Order was not deterministic for identic method names defined with @!method +# so we sort the list on path instead +def generate_method_list + @items = + prune_method_listing(Registry.all(:method), false) + .reject { |m| m.name.to_s =~ /=$/ && m.is_attribute? } + .sort_by { |m| m.path } + @list_title = "Method List" + @list_type = "method" + generate_list_contents +end diff --git a/documentation/yard-custom-template/default/layout/html/footer.erb b/documentation/yard-custom-template/default/layout/html/footer.erb new file mode 100644 index 00000000000..a38e33bcfdb --- /dev/null +++ b/documentation/yard-custom-template/default/layout/html/footer.erb @@ -0,0 +1,6 @@ +<%# Removes date and ruby version to avoid differences in CI check %> + diff --git a/documentation/yard-custom-template/default/method_details/setup.rb b/documentation/yard-custom-template/default/method_details/setup.rb new file mode 100644 index 00000000000..551e7fcbf33 --- /dev/null +++ b/documentation/yard-custom-template/default/method_details/setup.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +def source +end diff --git a/lib/tasks/documentation.rake b/lib/tasks/documentation.rake new file mode 100644 index 00000000000..1d74953eb1f --- /dev/null +++ b/lib/tasks/documentation.rake @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "fileutils" + +task "documentation" do + generate_chat_documentation +end + +def generate_chat_documentation + destination = File.join(Rails.root, "documentation/chat/frontend/") + config = File.join(Rails.root, ".jsdoc") + files = %w[ + plugins/chat/assets/javascripts/discourse/lib/collection.js + plugins/chat/assets/javascripts/discourse/pre-initializers/chat-plugin-api.js + plugins/chat/assets/javascripts/discourse/services/chat-api.js + ] + `yarn --silent jsdoc --readme plugins/chat/README.md -c #{config} #{files.join(" ")} -d #{destination}` + + # unecessary files + %w[ + documentation/chat/frontend/scripts/prism.min.js + documentation/chat/frontend/scripts/prism.js + documentation/chat/frontend/styles/vendor/prism-default.css + documentation/chat/frontend/styles/vendor/prism-okaidia.css + documentation/chat/frontend/styles/vendor/prism-tomorrow-night.css + ].each { |file| FileUtils.rm(file) } + + require "open3" + require "yard" + YARD::Templates::Engine.register_template_path( + File.join(Rails.root, "documentation", "yard-custom-template"), + ) + files = %w[ + plugins/chat/app/services/base.rb + plugins/chat/app/services/update_user_last_read.rb + plugins/chat/app/services/trash_channel.rb + plugins/chat/app/services/update_channel.rb + plugins/chat/app/services/update_channel_status.rb + ] + cmd = + "bundle exec yardoc -p documentation/yard-custom-template -t default -r plugins/chat/README.md --output-dir documentation/chat/backend #{files.join(" ")}" + Open3.popen3(cmd) { |_, stderr| puts stderr.read } +end diff --git a/package.json b/package.json index 922906ef928..6b49ab6ec6a 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,10 @@ "chrome-launcher": "^0.15.1", "chrome-remote-interface": "^0.31.3", "eslint-config-discourse": "^3.3.0", - "jsdoc-to-markdown": "^8.0.0", + "jsdoc": "^4.0.0", "lefthook": "^1.2.0", - "puppeteer-core": "^13.7.0" + "puppeteer-core": "^13.7.0", + "tidy-jsdoc": "^1.4.1" }, "scripts": { "postinstall": "yarn --cwd app/assets/javascripts/discourse $(node -e 'if(JSON.parse(process.env.npm_config_argv).original.includes(`--frozen-lockfile`)){console.log(`--frozen-lockfile`)}')" diff --git a/plugins/chat/README.md b/plugins/chat/README.md index fc0b204240b..a3d205fb72f 100644 --- a/plugins/chat/README.md +++ b/plugins/chat/README.md @@ -1,54 +1,9 @@ -:warning: This plugin is still in active development and may change frequently +This plugin is still in active development and may change frequently ## Documentation The Discourse Chat plugin adds chat functionality to your Discourse so it can natively support both long-form and short-form communication needs of your online community. -For documentation, see [Discourse Chat](https://meta.discourse.org/t/discourse-chat/230881) +For user documentation, see [Discourse Chat](https://meta.discourse.org/t/discourse-chat/230881). -## Plugin API - -### registerChatComposerButton - -#### Usage - -```javascript -api.registerChatComposerButton({ id: "foo", ... }); -``` - -#### Options - -Every option accepts a `value` or a `function`, when passing a function `this` will be the `chat-composer` component instance. Example of an option using a function: - -```javascript -api.registerChatComposerButton({ - id: "foo", - displayed() { - return this.site.mobileView && this.canAttachUploads; - }, -}); -``` - -##### Required - -- `id` unique, used to identify your button, eg: "gifs" -- `action` callback when the button is pressed, can be an action name or an anonymous function, eg: "onFooClicked" or `() => { console.log("clicked") }` - -A button requires at least an icon or a label: - -- `icon`, eg: "times" -- `label`, text displayed on the button, a translatable key, eg: "foo.bar" -- `translatedLabel`, text displayed on the button, a string, eg: "Add gifs" - -##### Optional - -- `position`, can be "inline" or "dropdown", defaults to "inline" -- `title`, title attribute of the button, a translatable key, eg: "foo.bar" -- `translatedTitle`, title attribute of the button, a string, eg: "Add gifs" -- `ariaLabel`, aria-label attribute of the button, a translatable key, eg: "foo.bar" -- `translatedAriaLabel`, aria-label attribute of the button, a string, eg: "Add gifs" -- `classNames`, additional names to add to the button’s class attribute, eg: ["foo", "bar"] -- `displayed`, hide/or show the button, expects a boolean -- `disabled`, sets the disabled attribute on the button, expects a boolean -- `priority`, an integer defining the order of the buttons, higher comes first, eg: `700` -- `dependentKeys`, list of property names which should trigger a refresh of the buttons when changed, eg: `["foo.bar", "bar.baz"]` +For developer documentation, see [Discourse Documentation](https://discourse.github.io/discourse/). diff --git a/plugins/chat/app/controllers/api/chat_channels_controller.rb b/plugins/chat/app/controllers/api/chat_channels_controller.rb index bf0618fc01c..992c58cd6e9 100644 --- a/plugins/chat/app/controllers/api/chat_channels_controller.rb +++ b/plugins/chat/app/controllers/api/chat_channels_controller.rb @@ -29,37 +29,9 @@ class Chat::Api::ChatChannelsController < Chat::Api end def destroy - confirmation = params.require(:channel).require(:name_confirmation)&.downcase - guardian.ensure_can_delete_chat_channel! - - if channel_from_params.title(current_user).downcase != confirmation - raise Discourse::InvalidParameters.new(:name_confirmation) + with_service Chat::Service::TrashChannel do + on_model_not_found(:channel) { raise ActiveRecord::RecordNotFound } end - - begin - ChatChannel.transaction do - channel_from_params.update!( - slug: - "#{Time.now.strftime("%Y%m%d-%H%M")}-#{channel_from_params.slug}-deleted".truncate( - SiteSetting.max_topic_title_length, - omission: "", - ), - ) - channel_from_params.trash!(current_user) - StaffActionLogger.new(current_user).log_custom( - "chat_channel_delete", - { - chat_channel_id: channel_from_params.id, - chat_channel_name: channel_from_params.title(current_user), - }, - ) - end - rescue ActiveRecord::Rollback - return render_json_error(I18n.t("chat.errors.delete_channel_failed")) - end - - Jobs.enqueue(:chat_channel_delete, { chat_channel_id: channel_from_params.id }) - render json: success_json end def create @@ -118,37 +90,25 @@ class Chat::Api::ChatChannelsController < Chat::Api end def update - guardian.ensure_can_edit_chat_channel! - - if channel_from_params.direct_message_channel? - raise Discourse::InvalidParameters.new( - I18n.t("chat.errors.cant_update_direct_message_channel"), - ) - end - params_to_edit = editable_params(params, channel_from_params) params_to_edit.each { |k, v| params_to_edit[k] = nil if params_to_edit[k].blank? } - if ActiveRecord::Type::Boolean.new.deserialize(params_to_edit[:auto_join_users]) auto_join_limiter(channel_from_params).performed! end - channel_from_params.update!(params_to_edit) - - ChatPublisher.publish_chat_channel_edit(channel_from_params, current_user) - - if channel_from_params.category_channel? && channel_from_params.auto_join_users - Chat::ChatChannelMembershipManager.new( - channel_from_params, - ).enforce_automatic_channel_memberships + with_service(Chat::Service::UpdateChannel, **params_to_edit) do + on_success do + render_serialized( + result.channel, + ChatChannelSerializer, + root: "channel", + membership: result.channel.membership_for(current_user), + ) + end + on_model_not_found(:channel) { raise ActiveRecord::RecordNotFound } + on_failed_policy(:check_channel_permission) { raise Discourse::InvalidAccess } + on_failed_policy(:no_direct_message_channel) { raise Discourse::InvalidAccess } end - - render_serialized( - channel_from_params, - ChatChannelSerializer, - root: "channel", - membership: channel_from_params.membership_for(current_user), - ) end private diff --git a/plugins/chat/app/controllers/api/chat_channels_status_controller.rb b/plugins/chat/app/controllers/api/chat_channels_status_controller.rb index 78b1ac3f2cb..863ee6f4f33 100644 --- a/plugins/chat/app/controllers/api/chat_channels_status_controller.rb +++ b/plugins/chat/app/controllers/api/chat_channels_status_controller.rb @@ -2,17 +2,10 @@ class Chat::Api::ChatChannelsStatusController < Chat::Api::ChatChannelsController def update - status = params.require(:status) - - # we only want to use this endpoint for open/closed status changes, - # the others are more "special" and are handled by the archive endpoint - if !ChatChannel.statuses.keys.include?(status) || status == "read_only" || status == "archive" - raise Discourse::InvalidParameters + with_service(Chat::Service::UpdateChannelStatus) do + on_success { render_serialized(result.channel, ChatChannelSerializer, root: "channel") } + on_model_not_found(:channel) { raise ActiveRecord::RecordNotFound } + on_failed_policy(:check_channel_permission) { raise Discourse::InvalidAccess } end - - guardian.ensure_can_change_channel_status!(channel_from_params, status.to_sym) - channel_from_params.public_send("#{status}!", current_user) - - render_serialized(channel_from_params, ChatChannelSerializer, root: "channel") end end diff --git a/plugins/chat/app/controllers/api_controller.rb b/plugins/chat/app/controllers/api_controller.rb index fa27b825d83..39f20e7c11a 100644 --- a/plugins/chat/app/controllers/api_controller.rb +++ b/plugins/chat/app/controllers/api_controller.rb @@ -4,6 +4,8 @@ class Chat::Api < Chat::ChatBaseController before_action :ensure_logged_in before_action :ensure_can_chat + include Chat::WithServiceHelper + private def ensure_can_chat diff --git a/plugins/chat/app/helpers/with_service_helper.rb b/plugins/chat/app/helpers/with_service_helper.rb new file mode 100644 index 00000000000..64e39787325 --- /dev/null +++ b/plugins/chat/app/helpers/with_service_helper.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +module Chat + module WithServiceHelper + def result + @_result + end + + def with_service(service, default_actions: true, **dependencies, &block) + controller = self + merged_block = + proc do + instance_eval(&controller.default_actions_for_service) if default_actions + instance_eval(&(block || proc {})) + end + Chat::Endpoint.call(service, controller, **dependencies, &merged_block) + end + + def run_service(service, dependencies) + @_result = service.call(params.to_unsafe_h.merge(guardian: guardian, **dependencies.to_h)) + end + + def default_actions_for_service + proc do + on_success { render(json: success_json) } + on_failure { render(json: failed_json, status: 422) } + on_failed_policy(:invalid_access) { raise Discourse::InvalidAccess } + on_failed_contract do + render( + json: + failed_json.merge(errors: result[:"result.contract.default"].errors.full_messages), + status: 400, + ) + end + end + end + end +end diff --git a/plugins/chat/app/models/chat_channel.rb b/plugins/chat/app/models/chat_channel.rb index c7ee81ea35d..beb38e15a8f 100644 --- a/plugins/chat/app/models/chat_channel.rb +++ b/plugins/chat/app/models/chat_channel.rb @@ -36,6 +36,10 @@ class ChatChannel < ActiveRecord::Base delegate :empty?, to: :chat_messages, prefix: true class << self + def editable_statuses + statuses.filter { |k, _| !%w[read_only archived].include?(k) } + end + def public_channel_chatable_types ["Category"] end diff --git a/plugins/chat/app/services/base.rb b/plugins/chat/app/services/base.rb new file mode 100644 index 00000000000..57d9a38abb9 --- /dev/null +++ b/plugins/chat/app/services/base.rb @@ -0,0 +1,427 @@ +# frozen_string_literal: true + +module Chat + module Service + # Module to be included to provide steps DSL to any class. This allows to + # create easy to understand services as the whole service cycle is visible + # simply by reading the beginning of its class. + # + # Steps are executed in the order they’re defined. They will use their name + # to execute the corresponding method defined in the service class. + # + # Currently, there are 5 types of steps: + # + # * +model(name = :model)+: used to instantiate a model (either by building + # it or fetching it from the DB). If a falsy value is returned, then the + # step will fail. Otherwise the resulting object will be assigned in + # +context[name]+ (+context[:model]+ by default). + # * +policy(name = :default)+: used to perform a check on the state of the + # system. Typically used to run guardians. If a falsy value is returned, + # the step will fail. + # * +contract(name = :default)+: used to validate the input parameters, + # typically provided by a user calling an endpoint. A special embedded + # +Contract+ class has to be defined to holds the validations. If the + # validations fail, the step will fail. Otherwise, the resulting contract + # will be available in +context[:contract]+. + # * +step(name)+: used to run small snippets of arbitrary code. The step + # doesn’t care about its return value, so to mark the service as failed, + # {#fail!} has to be called explicitly. + # * +transaction+: used to wrap other steps inside a DB transaction. + # + # The methods defined on the service are automatically provided with + # the whole context passed as keyword arguments. This allows to define in a + # very explicit way what dependencies are used by the method. If for + # whatever reason a key isn’t found in the current context, then Ruby will + # raise an exception when the method is called. + # + # Regarding contract classes, they have automatically {ActiveModel} modules + # included so all the {ActiveModel} API is available. + # + # @example An example from the {TrashChannel} service + # class TrashChannel + # include Base + # + # model :channel, :fetch_channel + # policy :invalid_access + # transaction do + # step :prevents_slug_collision + # step :soft_delete_channel + # step :log_channel_deletion + # end + # step :enqueue_delete_channel_relations_job + # + # private + # + # def fetch_channel(channel_id:, **) + # ChatChannel.find_by(id: channel_id) + # end + # + # def invalid_access(guardian:, channel:, **) + # guardian.can_preview_chat_channel?(channel) && guardian.can_delete_chat_channel? + # end + # + # def prevents_slug_collision(channel:, **) + # … + # end + # + # def soft_delete_channel(guardian:, channel:, **) + # … + # end + # + # def log_channel_deletion(guardian:, channel:, **) + # … + # end + # + # def enqueue_delete_channel_relations_job(channel:, **) + # … + # end + # end + # @example An example from the {UpdateChannelStatus} service which uses a contract + # class UpdateChannelStatus + # include Base + # + # model :channel, :fetch_channel + # contract + # policy :check_channel_permission + # step :change_status + # + # class Contract + # attribute :status + # validates :status, inclusion: { in: ChatChannel.editable_statuses.keys } + # end + # + # … + # end + module Base + extend ActiveSupport::Concern + + # The only exception that can be raised by a service. + class Failure < StandardError + # @return [Context] + attr_reader :context + + # @!visibility private + def initialize(context = nil) + @context = context + super + end + end + + # Simple structure to hold the context of the service during its whole lifecycle. + class Context < OpenStruct + # @return [Boolean] returns +true+ if the conext is set as successful (default) + def success? + !failure? + end + + # @return [Boolean] returns +true+ if the context is set as failed + # @see #fail! + # @see #fail + def failure? + @failure || false + end + + # Marks the context as failed. + # @param context [Hash, Context] the context to merge into the current one + # @example + # context.fail!("failure": "something went wrong") + # @return [Context] + def fail!(context = {}) + fail(context) + raise Failure, self + end + + # Marks the context as failed without raising an exception. + # @param context [Hash, Context] the context to merge into the current one + # @example + # context.fail("failure": "something went wrong") + # @return [Context] + def fail(context = {}) + merge(context) + @failure = true + self + end + + # Merges the given context into the current one. + # @!visibility private + def merge(other_context = {}) + other_context.each { |key, value| self[key.to_sym] = value } + self + end + + private + + def self.build(context = {}) + self === context ? context : new(context) + end + end + + # Internal module to define available steps as DSL + # @!visibility private + module StepsHelpers + def model(name = :model, step_name = :"fetch_#{name}") + steps << ModelStep.new(name, step_name) + end + + def contract(name = :default, class_name: self::Contract, default_values_from: nil) + steps << ContractStep.new( + name, + class_name: class_name, + default_values_from: default_values_from, + ) + end + + def policy(name = :default) + steps << PolicyStep.new(name) + end + + def step(name) + steps << Step.new(name) + end + + def transaction(&block) + steps << TransactionStep.new(&block) + end + end + + # @!visibility private + class Step + attr_reader :name, :method_name, :class_name + + def initialize(name, method_name = name, class_name: nil) + @name = name + @method_name = method_name + @class_name = class_name + end + + def call(instance, context) + method = instance.method(method_name) + args = {} + args = context.to_h unless method.arity.zero? + context[result_key] = Context.build + instance.instance_exec(**args, &method) + end + + private + + def type + self.class.name.split("::").last.downcase.sub(/^(\w+)step$/, "\\1") + end + + def result_key + "result.#{type}.#{name}" + end + end + + # @!visibility private + class ModelStep < Step + def call(instance, context) + context[name] = super + raise ArgumentError, "Model not found" unless context[name] + rescue ArgumentError => exception + context[result_key].fail(exception: exception) + context.fail! + end + end + + # @!visibility private + class PolicyStep < Step + def call(instance, context) + unless super + context[result_key].fail + context.fail! + end + end + end + + # @!visibility private + class ContractStep < Step + attr_reader :default_values_from + + def initialize(name, method_name = name, class_name: nil, default_values_from: nil) + super(name, method_name, class_name: class_name) + @default_values_from = default_values_from + end + + def call(instance, context) + attributes = class_name.attribute_names.map(&:to_sym) + default_values = {} + default_values = context[default_values_from].slice(*attributes) if default_values_from + contract = class_name.new(default_values.merge(context.to_h.slice(*attributes))) + context[contract_name] = contract + context[result_key] = Context.build + unless contract.valid? + context[result_key].fail(errors: contract.errors) + context.fail! + end + end + + private + + def contract_name + return :contract if name.to_sym == :default + :"#{name}_contract" + end + end + + # @!visibility private + class TransactionStep < Step + include StepsHelpers + + attr_reader :steps + + def initialize(&block) + @steps = [] + instance_exec(&block) + end + + def call(instance, context) + ActiveRecord::Base.transaction { steps.each { |step| step.call(instance, context) } } + end + end + + included do + # The global context which is available from any step. + attr_reader :context + + # @!visibility private + # Internal class used to setup the base contract of the service. + self::Contract = + Class.new do + include ActiveModel::API + include ActiveModel::Attributes + include ActiveModel::AttributeMethods + include ActiveModel::Validations::Callbacks + end + end + + class_methods do + include StepsHelpers + + def call(context = {}) + new(context).tap(&:run).context + end + + def call!(context = {}) + new(context).tap(&:run!).context + end + + def steps + @steps ||= [] + end + end + + # @!scope class + # @!method model(name = :model, step_name = :"fetch_#{name}") + # @param name [Symbol] name of the model + # @param step_name [Symbol] name of the method to call for this step + # Evaluates arbitrary code to build or fetch a model (typically from the + # DB). If the step returns a falsy value, then the step will fail. + # + # It stores the resulting model in +context[:model]+ by default (can be + # customized by providing the +name+ argument). + # + # @example + # model :channel, :fetch_channel + # + # private + # + # def fetch_channel(channel_id:, **) + # ChatChannel.find_by(id: channel_id) + # end + + # @!scope class + # @!method policy(name = :default) + # @param name [Symbol] name for this policy + # Performs checks related to the state of the system. If the + # step doesn’t return a truthy value, then the policy will fail. + # + # @example + # policy :no_direct_message_channel + # + # private + # + # def no_direct_message_channel(channel:, **) + # !channel.direct_message_channel? + # end + + # @!scope class + # @!method contract(name = :default, class_name: self::Contract, default_values_from: nil) + # @param name [Symbol] name for this contract + # @param class_name [Class] a class defining the contract + # @param default_values_from [Symbol] name of the model to get default values from + # Checks the validity of the input parameters. + # Implements ActiveModel::Validations and ActiveModel::Attributes. + # + # It stores the resulting contract in +context[:contract]+ by default + # (can be customized by providing the +name+ argument). + # + # @example + # contract + # + # class Contract + # attribute :name + # validates :name, presence: true + # end + + # @!scope class + # @!method step(name) + # @param name [Symbol] the name of this step + # Runs arbitrary code. To mark a step as failed, a call to {#fail!} needs + # to be made explicitly. + # + # @example + # step :update_channel + # + # private + # + # def update_channel(channel:, params_to_edit:, **) + # channel.update!(params_to_edit) + # end + # @example using {#fail!} in a step + # step :save_channel + # + # private + # + # def save_channel(channel:, **) + # fail!("something went wrong") unless channel.save + # end + + # @!scope class + # @!method transaction(&block) + # @param block [Proc] a block containing steps to be run inside a transaction + # Runs steps inside a DB transaction. + # + # @example + # transaction do + # step :prevents_slug_collision + # step :soft_delete_channel + # step :log_channel_deletion + # end + + # @!visibility private + def initialize(initial_context = {}) + @initial_context = initial_context.with_indifferent_access + @context = Context.build(initial_context.merge(__steps__: self.class.steps)) + end + + private + + def run + run! + rescue Failure => exception + raise if context.object_id != exception.context.object_id + end + + def run! + self.class.steps.each { |step| step.call(self, context) } + end + + def fail!(message) + step_name = caller_locations(1, 1)[0].label + context["result.step.#{step_name}"].fail(error: message) + context.fail! + end + end + end +end diff --git a/plugins/chat/app/services/trash_channel.rb b/plugins/chat/app/services/trash_channel.rb new file mode 100644 index 00000000000..48dde07f17f --- /dev/null +++ b/plugins/chat/app/services/trash_channel.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Chat + module Service + # Service responsible for trashing a chat channel. + # Note the slug is modified to prevent collisions. + # + # @example + # Chat::Service::TrashChannel.call(channel_id: 2, guardian: guardian) + # + class TrashChannel + include Base + + # @!method call(channel_id:, guardian:) + # @param [Integer] channel_id + # @param [Guardian] guardian + # @return [Chat::Service::Base::Context] + + DELETE_CHANNEL_LOG_KEY = "chat_channel_delete" + + model :channel, :fetch_channel + policy :invalid_access + transaction do + step :prevents_slug_collision + step :soft_delete_channel + step :log_channel_deletion + end + step :enqueue_delete_channel_relations_job + + private + + def fetch_channel(channel_id:, **) + ChatChannel.find_by(id: channel_id) + end + + def invalid_access(guardian:, channel:, **) + guardian.can_preview_chat_channel?(channel) && guardian.can_delete_chat_channel? + end + + def prevents_slug_collision(channel:, **) + channel.update!( + slug: + "#{Time.current.strftime("%Y%m%d-%H%M")}-#{channel.slug}-deleted".truncate( + SiteSetting.max_topic_title_length, + omission: "", + ), + ) + end + + def soft_delete_channel(guardian:, channel:, **) + channel.trash!(guardian.user) + end + + def log_channel_deletion(guardian:, channel:, **) + StaffActionLogger.new(guardian.user).log_custom( + DELETE_CHANNEL_LOG_KEY, + { chat_channel_id: channel.id, chat_channel_name: channel.title(guardian.user) }, + ) + end + + def enqueue_delete_channel_relations_job(channel:, **) + Jobs.enqueue(:chat_channel_delete, chat_channel_id: channel.id) + end + end + end +end diff --git a/plugins/chat/app/services/update_channel.rb b/plugins/chat/app/services/update_channel.rb new file mode 100644 index 00000000000..bcbb59b101e --- /dev/null +++ b/plugins/chat/app/services/update_channel.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Chat + module Service + # Service responsible for updating a chat channel's name, slug, and description. + # + # For a CategoryChannel, the settings for auto_join_users and allow_channel_wide_mentions + # are also editable. + # + # @example + # Chat::Service::UpdateChannel.call( + # channel_id: 2, + # guardian: guardian, + # name: "SuperChannel", + # description: "This is the best channel", + # slug: "super-channel", + # ) + # + class UpdateChannel + include Base + + # @!method call(channel_id:, guardian:, **params_to_edit) + # @param [Integer] channel_id + # @param [Guardian] guardian + # @param [Hash] params_to_edit + # @option params_to_edit [String,nil] name + # @option params_to_edit [String,nil] description + # @option params_to_edit [String,nil] slug + # @option params_to_edit [Boolean] auto_join_users Only valid for {CategoryChannel}. Whether active users + # with permission to see the category should automatically join the channel. + # @option params_to_edit [Boolean] allow_channel_wide_mentions Allow the use of @here and @all in the channel. + # @return [Chat::Service::Base::Context] + + model :channel, :fetch_channel + policy :no_direct_message_channel + policy :check_channel_permission + contract default_values_from: :channel + step :update_channel + step :publish_channel_update + step :auto_join_users_if_needed + + # @!visibility private + class Contract + attribute :name, :string + attribute :description, :string + attribute :slug, :string + attribute :auto_join_users, :boolean, default: false + attribute :allow_channel_wide_mentions, :boolean, default: true + + before_validation do + assign_attributes( + attributes + .symbolize_keys + .slice(:name, :description, :slug) + .transform_values(&:presence), + ) + end + end + + private + + def fetch_channel(channel_id:, **) + ChatChannel.find_by(id: channel_id) + end + + def no_direct_message_channel(channel:, **) + !channel.direct_message_channel? + end + + def check_channel_permission(guardian:, channel:, **) + guardian.can_preview_chat_channel?(channel) && guardian.can_edit_chat_channel? + end + + def update_channel(channel:, contract:, **) + channel.update!(contract.attributes) + end + + def publish_channel_update(channel:, guardian:, **) + ChatPublisher.publish_chat_channel_edit(channel, guardian.user) + end + + def auto_join_users_if_needed(channel:, **) + return unless channel.auto_join_users? + Chat::ChatChannelMembershipManager.new(channel).enforce_automatic_channel_memberships + end + end + end +end diff --git a/plugins/chat/app/services/update_channel_status.rb b/plugins/chat/app/services/update_channel_status.rb new file mode 100644 index 00000000000..e11e80c6eed --- /dev/null +++ b/plugins/chat/app/services/update_channel_status.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Chat + module Service + # Service responsible for updating a chat channel status. + # + # @example + # Chat::Service::UpdateChannelStatus.call(channel_id: 2, guardian: guardian, status: "open") + # + class UpdateChannelStatus + include Base + + # @!method call(channel_id:, guardian:, status:) + # @param [Integer] channel_id + # @param [Guardian] guardian + # @param [String] status + # @return [Chat::Service::Base::Context] + + model :channel, :fetch_channel + contract + policy :check_channel_permission + step :change_status + + # @!visibility private + class Contract + attribute :status + validates :status, inclusion: { in: ChatChannel.editable_statuses.keys } + end + + private + + def fetch_channel(channel_id:, **) + ChatChannel.find_by(id: channel_id) + end + + def check_channel_permission(guardian:, channel:, status:, **) + guardian.can_preview_chat_channel?(channel) && + guardian.can_change_channel_status?(channel, status.to_sym) + end + + def change_status(channel:, status:, guardian:, **) + channel.public_send("#{status}!", guardian.user) + end + end + end +end diff --git a/plugins/chat/app/services/update_user_last_read.rb b/plugins/chat/app/services/update_user_last_read.rb new file mode 100644 index 00000000000..45b0a33050c --- /dev/null +++ b/plugins/chat/app/services/update_user_last_read.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Chat + module Service + # Service responsible for updating the last read message id of a membership. + # + # @example + # Chat::Service::UpdateUserLastRead.call(user_id: 1, channel_id: 2, message_id: 3, guardian: guardian) + # + class UpdateUserLastRead + include Base + + # @!method call(user_id:, channel_id:, message_id:, guardian:) + # @param [Integer] user_id + # @param [Integer] channel_id + # @param [Integer] message_id + # @param [Guardian] guardian + # @return [Chat::Service::Base::Context] + + model :membership, :fetch_active_membership + policy :invalid_access + contract + policy :ensure_message_id_recency + policy :ensure_message_exists + step :update_last_read_message_id + step :mark_associated_mentions_as_read + step :publish_new_last_read_to_clients + + # @!visibility private + class Contract + attribute :message_id, :integer + attribute :user_id, :integer + attribute :channel_id, :integer + end + + private + + def fetch_active_membership(user_id:, channel_id:, **) + UserChatChannelMembership.includes(:user, :chat_channel).find_by( + user_id: user_id, + chat_channel_id: channel_id, + following: true, + ) + end + + def invalid_access(guardian:, membership:, **) + guardian.can_join_chat_channel?(membership.chat_channel) + end + + def ensure_message_id_recency(message_id:, membership:, **) + !membership.last_read_message_id || message_id >= membership.last_read_message_id + end + + def ensure_message_exists(channel_id:, message_id:, **) + ChatMessage.with_deleted.exists?(chat_channel_id: channel_id, id: message_id) + end + + def update_last_read_message_id(message_id:, membership:, **) + membership.update!(last_read_message_id: message_id) + end + + def mark_associated_mentions_as_read(membership:, message_id:, **) + Notification + .where(notification_type: Notification.types[:chat_mention]) + .where(user: membership.user) + .where(read: false) + .joins("INNER JOIN chat_mentions ON chat_mentions.notification_id = notifications.id") + .joins("INNER JOIN chat_messages ON chat_mentions.chat_message_id = chat_messages.id") + .where("chat_messages.id <= ?", message_id) + .where("chat_messages.chat_channel_id = ?", membership.chat_channel.id) + .update_all(read: true) + end + + def publish_new_last_read_to_clients(guardian:, channel_id:, message_id:, **) + ChatPublisher.publish_user_tracking_state(guardian.user, channel_id, message_id) + end + end + end +end diff --git a/plugins/chat/assets/javascripts/discourse/lib/collection.js b/plugins/chat/assets/javascripts/discourse/lib/collection.js index a001121e2d6..8fea367f766 100644 --- a/plugins/chat/assets/javascripts/discourse/lib/collection.js +++ b/plugins/chat/assets/javascripts/discourse/lib/collection.js @@ -1,5 +1,3 @@ -/** @module Collection */ - import { ajax } from "discourse/lib/ajax"; import { tracked } from "@glimmer/tracking"; import { bind } from "discourse-common/utils/decorators"; @@ -7,19 +5,12 @@ import { Promise } from "rsvp"; /** * Handles a paginated API response. - * - * @class */ export default class Collection { @tracked items = []; @tracked meta = {}; @tracked loading = false; - /** - * Create a Collection instance - * @param {string} resourceURL - the API endpoint to call - * @param {callback} handler - anonymous function used to handle the response - */ constructor(resourceURL, handler) { this._resourceURL = resourceURL; this._handler = handler; diff --git a/plugins/chat/assets/javascripts/discourse/pre-initializers/chat-plugin-api.js b/plugins/chat/assets/javascripts/discourse/pre-initializers/chat-plugin-api.js index 3a6801ea0b7..15c94c5ff66 100644 --- a/plugins/chat/assets/javascripts/discourse/pre-initializers/chat-plugin-api.js +++ b/plugins/chat/assets/javascripts/discourse/pre-initializers/chat-plugin-api.js @@ -5,6 +5,66 @@ import { } from "discourse/plugins/chat/discourse/components/chat-message"; import { registerChatComposerButton } from "discourse/plugins/chat/discourse/lib/chat-composer-buttons"; +/** + * Class exposing the javascript API available to plugins and themes. + * @class PluginApi + */ + +/** + * Callback used to decorate a chat message + * + * @callback PluginApi~decorateChatMessageCallback + * @param {ChatMessage} chatMessage - model + * @param {HTMLElement} messageContainer - DOM node + * @param {ChatChannel} chatChannel - model + */ + +/** + * Decorate a chat message + * + * @memberof PluginApi + * @instance + * @function decorateChatMessage + * @param {PluginApi~decorateChatMessageCallback} decorator + * @example + * + * api.decorateChatMessage((chatMessage, messageContainer) => { + * messageContainer.dataset.foo = chatMessage.id; + * }); + */ + +/** + * Register a button in the chat composer + * + * @memberof PluginApi + * @instance + * @function registerChatComposerButton + * @param {Object} options + * @param {number} options.id - The id of the button + * @param {function} options.action - An action name or an anonymous function called when the button is pressed, eg: "onFooClicked" or `() => { console.log("clicked") }` + * @param {string} options.icon - A valid font awesome icon name, eg: "far fa-image" + * @param {string} options.label - Text displayed on the button, a translatable key, eg: "foo.bar" + * @param {string} options.translatedLabel - Text displayed on the button, a string, eg: "Add gifs" + * @param {string} [options.position] - Can be "inline" or "dropdown", defaults to "inline" + * @param {string} [options.title] - Title attribute of the button, a translatable key, eg: "foo.bar" + * @param {string} [options.translatedTitle] - Title attribute of the button, a string, eg: "Add gifs" + * @param {string} [options.ariaLabel] - aria-label attribute of the button, a translatable key, eg: "foo.bar" + * @param {string} [options.translatedAriaLabel] - aria-label attribute of the button, a string, eg: "Add gifs" + * @param {string} [options.classNames] - Additional names to add to the button’s class attribute, eg: ["foo", "bar"] + * @param {boolean} [options.displayed] - Hide or show the button + * @param {boolean} [options.disabled] - Sets the disabled attribute on the button + * @param {number} [options.priority] - An integer defining the order of the buttons, higher comes first, eg: `700` + * @param {Array.} [options.dependentKeys] - List of property names which should trigger a refresh of the buttons when changed, eg: `["foo.bar", "bar.baz"]` + * @example + * + * api.registerChatComposerButton({ + * id: "foo", + * displayed() { + * return this.site.mobileView && this.canAttachUploads; + * } + * }); + */ + export default { name: "chat-plugin-api", after: "inject-discourse-objects", diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-decorator.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-decorator.js index 1d7dc38e9f9..53e16e8a6e0 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-decorator.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-decorator.js @@ -14,7 +14,8 @@ export default function withChatChannel(extendedClass) { this.controllerFor("chat-channel").set("targetMessageId", null); this.chat.activeChannel = model; - let { messageId } = this.paramsFor(this.routeName); + let { messageId, channelTitle } = this.paramsFor(this.routeName); + // messageId query param backwards-compatibility if (messageId) { this.router.replaceWith( @@ -24,7 +25,6 @@ export default function withChatChannel(extendedClass) { ); } - const { channelTitle } = this.paramsFor("chat.channel"); if (channelTitle && channelTitle !== model.slugifiedTitle) { const nearMessageParams = this.paramsFor("chat.channel.near-message"); if (nearMessageParams.messageId) { diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js index e0157f0f617..7fd532dfa5c 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-api.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js @@ -1,5 +1,3 @@ -/** @module ChatApi */ - import Service, { inject as service } from "@ember/service"; import { ajax } from "discourse/lib/ajax"; import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership"; @@ -8,7 +6,7 @@ import Collection from "../lib/collection"; /** * Chat API service. Provides methods to interact with the chat API. * - * @class + * @module ChatApi * @implements {@ember/service} */ export default class ChatApi extends Service { @@ -31,7 +29,7 @@ export default class ChatApi extends Service { /** * List all accessible category channels of the current user. - * @returns {module:Collection} + * @returns {Collection} * * @example * @@ -70,17 +68,14 @@ export default class ChatApi extends Service { /** * Destroys a channel. * @param {number} channelId - The ID of the channel. - * @param {string} channelName - The name of the channel to be destroyed, used as confirmation. * @returns {Promise} * * @example * - * this.chatApi.destroyChannel(1, "foo").then(() => { ... }) + * this.chatApi.destroyChannel(1).then(() => { ... }) */ - destroyChannel(channelId, channelName) { - return this.#deleteRequest(`/channels/${channelId}`, { - channel: { name_confirmation: channelName }, - }); + destroyChannel(channelId) { + return this.#deleteRequest(`/channels/${channelId}`); } /** @@ -174,7 +169,7 @@ export default class ChatApi extends Service { /** * Lists members of a channel. * @param {number} channelId - The ID of the channel. - * @returns {module:Collection} + * @returns {Collection} */ listChannelMemberships(channelId) { return new Collection( diff --git a/plugins/chat/docs/FRONTEND.md b/plugins/chat/docs/FRONTEND.md deleted file mode 100644 index 8d81aafc507..00000000000 --- a/plugins/chat/docs/FRONTEND.md +++ /dev/null @@ -1,352 +0,0 @@ -## Modules - -
-
Collection
-
-
ChatApi
-
-
- - - -## Collection - -* [Collection](#module_Collection) - * [module.exports](#exp_module_Collection--module.exports) ⏏ - * [new module.exports(resourceURL, handler)](#new_module_Collection--module.exports_new) - * [.load()](#module_Collection--module.exports+load) ⇒ Promise - * [.loadMore()](#module_Collection--module.exports+loadMore) ⇒ Promise - - -* * * - - - -### module.exports ⏏ -Handles a paginated API response. - -**Kind**: Exported class - -* * * - - - -#### new module.exports(resourceURL, handler) -Create a Collection instance - - -| Param | Type | Description | -| --- | --- | --- | -| resourceURL | string | the API endpoint to call | -| handler | callback | anonymous function used to handle the response | - - -* * * - - - -#### module.exports.load() ⇒ Promise -Loads first batch of results - -**Kind**: instance method of [module.exports](#exp_module_Collection--module.exports) - -* * * - - - -#### module.exports.loadMore() ⇒ Promise -Attempts to load more results - -**Kind**: instance method of [module.exports](#exp_module_Collection--module.exports) - -* * * - - - -## ChatApi - -* [ChatApi](#module_ChatApi) - * [module.exports](#exp_module_ChatApi--module.exports) ⏏ - * [.channel(channelId)](#module_ChatApi--module.exports+channel) ⇒ Promise - * [.channels()](#module_ChatApi--module.exports+channels) ⇒ [module.exports](#exp_module_Collection--module.exports) - * [.moveChannelMessages(channelId, data)](#module_ChatApi--module.exports+moveChannelMessages) ⇒ Promise - * [.destroyChannel(channelId, channelName)](#module_ChatApi--module.exports+destroyChannel) ⇒ Promise - * [.createChannel(data)](#module_ChatApi--module.exports+createChannel) ⇒ Promise - * [.categoryPermissions(categoryId)](#module_ChatApi--module.exports+categoryPermissions) ⇒ Promise - * [.sendMessage(channelId, data)](#module_ChatApi--module.exports+sendMessage) ⇒ Promise - * [.createChannelArchive(channelId, data)](#module_ChatApi--module.exports+createChannelArchive) ⇒ Promise - * [.updateChannel(channelId, data)](#module_ChatApi--module.exports+updateChannel) ⇒ Promise - * [.updateChannelStatus(channelId, status)](#module_ChatApi--module.exports+updateChannelStatus) ⇒ Promise - * [.listChannelMemberships(channelId)](#module_ChatApi--module.exports+listChannelMemberships) ⇒ [module.exports](#exp_module_Collection--module.exports) - * [.listCurrentUserChannels()](#module_ChatApi--module.exports+listCurrentUserChannels) ⇒ Promise - * [.followChannel(channelId)](#module_ChatApi--module.exports+followChannel) ⇒ Promise - * [.unfollowChannel(channelId)](#module_ChatApi--module.exports+unfollowChannel) ⇒ Promise - * [.updateCurrentUserChannelNotificationsSettings(channelId, data)](#module_ChatApi--module.exports+updateCurrentUserChannelNotificationsSettings) ⇒ Promise - - -* * * - - - -### module.exports ⏏ -Chat API service. Provides methods to interact with the chat API. - -**Kind**: Exported class -**Implements**: {@ember/service} - -* * * - - - -#### module.exports.channel(channelId) ⇒ Promise -Get a channel by its ID. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | The ID of the channel. | - -**Example** -```js -this.chatApi.channel(1).then(channel => { ... }) -``` - -* * * - - - -#### module.exports.channels() ⇒ [module.exports](#exp_module_Collection--module.exports) -List all accessible category channels of the current user. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) -**Example** -```js -this.chatApi.channels.then(channels => { ... }) -``` - -* * * - - - -#### module.exports.moveChannelMessages(channelId, data) ⇒ Promise -Moves messages from one channel to another. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | The ID of the original channel. | -| data | object | Params of the move. | -| data.message_ids | Array.<number> | IDs of the moved messages. | -| data.destination_channel_id | number | ID of the channel where the messages are moved to. | - -**Example** -```js -this.chatApi - .moveChannelMessages(1, { - message_ids: [2, 3], - destination_channel_id: 4, - }).then(() => { ... }) -``` - -* * * - - - -#### module.exports.destroyChannel(channelId, channelName) ⇒ Promise -Destroys a channel. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | The ID of the channel. | -| channelName | string | The name of the channel to be destroyed, used as confirmation. | - -**Example** -```js -this.chatApi.destroyChannel(1, "foo").then(() => { ... }) -``` - -* * * - - - -#### module.exports.createChannel(data) ⇒ Promise -Creates a channel. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| data | object | Params of the channel. | -| data.name | string | The name of the channel. | -| data.chatable_id | string | The category of the channel. | -| data.description | string | The description of the channel. | -| [data.auto_join_users] | boolean | Should users join this channel automatically. | - -**Example** -```js -this.chatApi - .createChannel({ name: "foo", chatable_id: 1, description "bar" }) - .then((channel) => { ... }) -``` - -* * * - - - -#### module.exports.categoryPermissions(categoryId) ⇒ Promise -Lists chat permissions for a category. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| categoryId | number | ID of the category. | - - -* * * - - - -#### module.exports.sendMessage(channelId, data) ⇒ Promise -Sends a message. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | ID of the channel. | -| data | object | Params of the message. | -| data.message | string | The raw content of the message in markdown. | -| data.cooked | string | The cooked content of the message. | -| [data.in_reply_to_id] | number | The ID of the replied-to message. | -| [data.staged_id] | number | The staged ID of the message before it was persisted. | -| [data.upload_ids] | Array.<number> | Array of upload ids linked to the message. | - - -* * * - - - -#### module.exports.createChannelArchive(channelId, data) ⇒ Promise -Creates a channel archive. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | The ID of the channel. | -| data | object | Params of the archive. | -| data.selection | string | "new_topic" or "existing_topic". | -| [data.title] | string | Title of the topic when creating a new topic. | -| [data.category_id] | string | ID of the category used when creating a new topic. | -| [data.tags] | Array.<string> | tags used when creating a new topic. | -| [data.topic_id] | string | ID of the topic when using an existing topic. | - - -* * * - - - -#### module.exports.updateChannel(channelId, data) ⇒ Promise -Updates a channel. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | The ID of the channel. | -| data | object | Params of the archive. | -| [data.description] | string | Description of the channel. | -| [data.name] | string | Name of the channel. | - - -* * * - - - -#### module.exports.updateChannelStatus(channelId, status) ⇒ Promise -Updates the status of a channel. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | The ID of the channel. | -| status | string | The new status, can be "open" or "closed". | - - -* * * - - - -#### module.exports.listChannelMemberships(channelId) ⇒ [module.exports](#exp_module_Collection--module.exports) -Lists members of a channel. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | The ID of the channel. | - - -* * * - - - -#### module.exports.listCurrentUserChannels() ⇒ Promise -Lists public and direct message channels of the current user. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -* * * - - - -#### module.exports.followChannel(channelId) ⇒ Promise -Makes current user follow a channel. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | The ID of the channel. | - - -* * * - - - -#### module.exports.unfollowChannel(channelId) ⇒ Promise -Makes current user unfollow a channel. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | The ID of the channel. | - - -* * * - - - -#### module.exports.updateCurrentUserChannelNotificationsSettings(channelId, data) ⇒ Promise -Update notifications settings of current user for a channel. - -**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) - -| Param | Type | Description | -| --- | --- | --- | -| channelId | number | The ID of the channel. | -| data | object | The settings to modify. | -| [data.muted] | boolean | Mutes the channel. | -| [data.desktop_notification_level] | string | Notifications level on desktop: never, mention or always. | -| [data.mobile_notification_level] | string | Notifications level on mobile: never, mention or always. | - - -* * * - diff --git a/plugins/chat/lib/endpoint.rb b/plugins/chat/lib/endpoint.rb new file mode 100644 index 00000000000..bddb987ef24 --- /dev/null +++ b/plugins/chat/lib/endpoint.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true +# +# = Chat::Endpoint +# +# This class is to be used via its helper +with_service+ in a controller. Its +# main purpose is to ease how actions can be run upon a service completion. +# Since a service will likely return the same kind of things over and over, +# this allows us to not have to repeat the same boilerplate code in every +# controller. +# +# There are several available actions and we can add new ones very easily: +# +# * +on_success+: will execute the provided block if the service succeeds +# * +on_failure+: will execute the provided block if the service fails +# * +on_failed_policy(name)+: will execute the provided block if the policy +# named `name` fails +# * +on_failed_contract(name)+: will execute the provided block if the contract +# named `name` fails +# * +on_model_not_found(name)+: will execute the provided block if the service +# fails and its model is not present +# +# @example +# # in a controller +# def create +# with_service MyService do +# on_success do +# flash[:notice] = "Success!" +# redirect_to a_path +# end +# on_failed_policy(:a_named_policy) { redirect_to root_path } +# on_failure { render :new } +# end +# end +# +# The actions will be evaluated in the order they appear. So even if the +# service will ultimately fail with a failed policy, in this example only the +# +on_failed_policy+ action will be executed and not the +on_failure+ one. +# The only exception to this being +on_failure+ as it will always be executed +# last. +# +class Chat::Endpoint + # @!visibility private + NULL_RESULT = OpenStruct.new(failure?: false) + # @!visibility private + AVAILABLE_ACTIONS = { + on_success: -> { result.success? }, + on_failure: -> { result.failure? }, + on_failed_policy: ->(name = "default") { failure_for?("result.policy.#{name}") }, + on_failed_contract: ->(name = "default") { failure_for?("result.contract.#{name}") }, + on_model_not_found: ->(name = "model") { failure_for?("result.model.#{name}") }, + }.with_indifferent_access.freeze + + # @!visibility private + attr_reader :service, :controller, :dependencies + + delegate :result, to: :controller + + # @!visibility private + def initialize(service, controller, **dependencies) + @service = service + @controller = controller + @dependencies = dependencies + @actions = {} + end + + # @param service [Class] a class including {Chat::Service::Base} + # @param block [Proc] a block containing the steps to match on + # @return [void] + def self.call(service, controller, **dependencies, &block) + new(service, controller, **dependencies).call(&block) + end + + # @!visibility private + def call(&block) + instance_eval(&block) + controller.run_service(service, dependencies) + # Always have `on_failure` as the last action + ( + actions + .except(:on_failure) + .merge(actions.slice(:on_failure)) + .detect { |name, (condition, _)| condition.call } || [-> {}] + ).flatten.last.call + end + + private + + attr_reader :actions + + def failure_for?(key) + (controller.result[key] || NULL_RESULT).failure? + end + + def add_action(name, *args, &block) + actions[[name, *args].join("_").to_sym] = [ + -> { instance_exec(*args, &AVAILABLE_ACTIONS[name]) }, + -> { controller.instance_eval(&block) }, + ] + end + + def method_missing(method_name, *args, &block) + return super unless AVAILABLE_ACTIONS[method_name] + add_action(method_name, *args, &block) + end + + def respond_to_missing?(method_name, include_private = false) + AVAILABLE_ACTIONS[method_name] || super + end +end diff --git a/plugins/chat/lib/guardian_extensions.rb b/plugins/chat/lib/guardian_extensions.rb index d539c122548..e57f7c5927b 100644 --- a/plugins/chat/lib/guardian_extensions.rb +++ b/plugins/chat/lib/guardian_extensions.rb @@ -61,6 +61,7 @@ module Chat::GuardianExtensions return false if chat_channel.status.to_sym == target_status.to_sym return false if !is_staff? + # FIXME: This logic shouldn't be handled in guardian case target_status when :closed chat_channel.open? diff --git a/plugins/chat/lib/steps_inspector.rb b/plugins/chat/lib/steps_inspector.rb new file mode 100644 index 00000000000..1291a95eaf6 --- /dev/null +++ b/plugins/chat/lib/steps_inspector.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +module Chat + # = Chat::StepsInspector + # + # This class takes a {Chat::Service::Base::Context} object and inspects it. + # It will output a list of steps and what is their known state. + class StepsInspector + # @!visibility private + class Step + attr_reader :step, :result, :nesting_level + + delegate :name, to: :step + delegate :failure?, :success?, :error, to: :step_result, allow_nil: true + + def self.for(step, result, nesting_level: 0) + class_name = + "#{module_parent_name}::#{step.class.name.split("::").last.sub(/^(\w+)Step$/, "\\1")}" + class_name.constantize.new(step, result, nesting_level: nesting_level) + end + + def initialize(step, result, nesting_level: 0) + @step = step + @result = result + @nesting_level = nesting_level + end + + def type + self.class.name.split("::").last.downcase + end + + def emoji + return "❌" if failure? + return "✅" if success? + "" + end + + def steps + [self] + end + + def inspect + "#{" " * nesting_level}[#{type}] '#{name}' #{emoji}" + end + + private + + def step_result + result["result.#{type}.#{name}"] + end + end + + # @!visibility private + class Model < Step + def error + step_result.exception.full_message + end + end + + # @!visibility private + class Contract < Step + def error + step_result.errors.inspect + end + end + + # @!visibility private + class Policy < Step + end + + # @!visibility private + class Transaction < Step + def steps + [self, *step.steps.map { Step.for(_1, result, nesting_level: nesting_level + 1).steps }] + end + + def inspect + "#{" " * nesting_level}[#{type}]" + end + + def step_result + nil + end + end + + attr_reader :steps, :result + + def initialize(result) + @steps = result.__steps__.map { Step.for(_1, result).steps }.flatten + @result = result + end + + # Inspect the provided result object. + # Example output: + # [1/4] [model] 'channel' ✅ + # [2/4] [contract] 'default' ✅ + # [3/4] [policy] 'check_channel_permission' ❌ + # [4/4] [step] 'change_status' + # @return [String] the steps of the result object with their state + def inspect + steps + .map + .with_index { |step, index| "[#{index + 1}/#{steps.size}] #{step.inspect}" } + .join("\n") + end + + # @return [String, nil] the first available error, if any. + def error + steps.detect(&:failure?)&.error + end + end +end diff --git a/plugins/chat/lib/tasks/chat_doc.rake b/plugins/chat/lib/tasks/chat_doc.rake deleted file mode 100644 index 98fb9553e3b..00000000000 --- a/plugins/chat/lib/tasks/chat_doc.rake +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -task "chat:doc" do - destination = File.join(Rails.root, "plugins/chat/docs/FRONTEND.md") - config = File.join(Rails.root, ".jsdoc") - - files = %w[ - plugins/chat/assets/javascripts/discourse/lib/collection.js - plugins/chat/assets/javascripts/discourse/services/chat-api.js - ] - - `yarn --silent jsdoc2md --separators -c #{config} -f #{files.join(" ")} > #{destination}` -end diff --git a/plugins/chat/plugin.rb b/plugins/chat/plugin.rb index 329d07209ed..16b2c8e93ae 100644 --- a/plugins/chat/plugin.rb +++ b/plugins/chat/plugin.rb @@ -91,6 +91,7 @@ require_relative "app/core_ext/plugin_instance.rb" GlobalSetting.add_default(:allow_unsecure_chat_uploads, false) after_initialize do + # Namespace for classes and modules parts of chat plugin module ::Chat PLUGIN_NAME = "chat" HAS_CHAT_ENABLED = "has_chat_enabled" @@ -119,6 +120,7 @@ after_initialize do "../app/controllers/admin/admin_incoming_chat_webhooks_controller.rb", __FILE__, ) + load File.expand_path("../app/helpers/with_service_helper.rb", __FILE__) load File.expand_path("../app/controllers/chat_base_controller.rb", __FILE__) load File.expand_path("../app/controllers/chat_controller.rb", __FILE__) load File.expand_path("../app/controllers/emojis_controller.rb", __FILE__) @@ -163,6 +165,7 @@ after_initialize do load File.expand_path("../app/serializers/admin_chat_index_serializer.rb", __FILE__) load File.expand_path("../app/serializers/user_chat_message_bookmark_serializer.rb", __FILE__) load File.expand_path("../app/serializers/reviewable_chat_message_serializer.rb", __FILE__) + load File.expand_path("../app/services/base.rb", __FILE__) load File.expand_path("../lib/chat_channel_fetcher.rb", __FILE__) load File.expand_path("../lib/chat_channel_hashtag_data_source.rb", __FILE__) load File.expand_path("../lib/chat_mailer.rb", __FILE__) @@ -191,6 +194,8 @@ after_initialize do load File.expand_path("../lib/slack_compatibility.rb", __FILE__) load File.expand_path("../lib/post_notification_handler.rb", __FILE__) load File.expand_path("../lib/secure_uploads_compatibility.rb", __FILE__) + load File.expand_path("../lib/endpoint.rb", __FILE__) + load File.expand_path("../lib/steps_inspector.rb", __FILE__) load File.expand_path("../app/jobs/regular/auto_manage_channel_memberships.rb", __FILE__) load File.expand_path("../app/jobs/regular/auto_join_channel_batch.rb", __FILE__) load File.expand_path("../app/jobs/regular/process_chat_message.rb", __FILE__) @@ -207,7 +212,11 @@ after_initialize do load File.expand_path("../app/jobs/scheduled/auto_join_users.rb", __FILE__) load File.expand_path("../app/jobs/scheduled/chat_periodical_updates.rb", __FILE__) load File.expand_path("../app/services/chat_publisher.rb", __FILE__) + load File.expand_path("../app/services/trash_channel.rb", __FILE__) + load File.expand_path("../app/services/update_channel.rb", __FILE__) + load File.expand_path("../app/services/update_channel_status.rb", __FILE__) load File.expand_path("../app/services/chat_message_destroyer.rb", __FILE__) + load File.expand_path("../app/services/update_user_last_read.rb", __FILE__) load File.expand_path("../app/controllers/api_controller.rb", __FILE__) load File.expand_path("../app/controllers/api/chat_channels_controller.rb", __FILE__) load File.expand_path("../app/controllers/api/chat_current_user_channels_controller.rb", __FILE__) diff --git a/plugins/chat/spec/fabricators/chat_fabricator.rb b/plugins/chat/spec/fabricators/chat_fabricator.rb index 9c2cf3f7e39..3602998622e 100644 --- a/plugins/chat/spec/fabricators/chat_fabricator.rb +++ b/plugins/chat/spec/fabricators/chat_fabricator.rb @@ -59,9 +59,50 @@ Fabricator(:chat_message) do end Fabricator(:chat_mention) do - chat_message { Fabricate(:chat_message) } + transient read: false + transient high_priority: true + transient identifier: :direct_mentions + user { Fabricate(:user) } - notification { Fabricate(:notification) } + chat_message { Fabricate(:chat_message) } + notification do |attrs| + # All this setup should be in a service we could just call here + # At the moment the logic is all split in a job + channel = attrs[:chat_message].chat_channel + + payload = { + is_direct_message_channel: channel.direct_message_channel?, + mentioned_by_username: attrs[:chat_message].user.username, + chat_channel_id: channel.id, + chat_message_id: attrs[:chat_message].id, + } + + if channel.direct_message_channel? + payload[:chat_channel_title] = channel.title(membership.user) + payload[:chat_channel_slug] = channel.slug + end + + unless attrs[:identifier] == :direct_mentions + case attrs[:identifier] + when :here_mentions + payload[:identifier] = "here" + when :global_mentions + payload[:identifier] = "all" + else + payload[:identifier] = attrs[:identifier] if attrs[:identifier] + payload[:is_group_mention] = true + end + end + + Fabricate( + :notification, + notification_type: Notification.types[:chat_mention], + user: attrs[:user], + data: payload.to_json, + read: attrs[:read], + high_priority: attrs[:high_priority], + ) + end end Fabricator(:chat_message_reaction) do diff --git a/plugins/chat/spec/lib/endpoint_spec.rb b/plugins/chat/spec/lib/endpoint_spec.rb new file mode 100644 index 00000000000..26a00661d8d --- /dev/null +++ b/plugins/chat/spec/lib/endpoint_spec.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true + +RSpec.describe Chat::Endpoint do + class SuccessService + include Chat::Service::Base + end + + class FailureService + include Chat::Service::Base + + step :fail_step + + def fail_step + fail!("error") + end + end + + class FailedPolicyService + include Chat::Service::Base + + policy :test + + def test + false + end + end + + class SuccessPolicyService + include Chat::Service::Base + + policy :test + + def test + true + end + end + + class FailedContractService + include Chat::Service::Base + + class Contract + attribute :test + validates :test, presence: true + end + + contract + end + + class SuccessContractService + include Chat::Service::Base + + contract + end + + class FailureWithModelService + include Chat::Service::Base + + model :fake_model, :fetch_fake_model + + private + + def fetch_fake_model + nil + end + end + + class SuccessWithModelService + include Chat::Service::Base + + model :fake_model, :fetch_fake_model + + private + + def fetch_fake_model + :model_found + end + end + + describe ".call(service, &block)" do + subject(:endpoint) { described_class.call(service, controller, &actions_block) } + + let(:result) { controller.result } + let(:actions_block) { controller.instance_eval(actions) } + let(:service) { SuccessService } + let(:actions) { "proc {}" } + let(:controller) do + Class + .new(Chat::Api) do + def request + OpenStruct.new + end + + def params + ActionController::Parameters.new + end + + def guardian + end + end + .new + end + + it "runs the provided service in the context of a controller" do + endpoint + expect(result).to be_a Chat::Service::Base::Context + expect(result).to be_a_success + end + + context "when using the on_success action" do + let(:actions) { <<-BLOCK } + proc do + on_success { :success } + end + BLOCK + + context "when the service succeeds" do + it "runs the provided block" do + expect(endpoint).to eq :success + end + end + + context "when the service does not succeed" do + let(:service) { FailureService } + + it "does not run the provided block" do + expect(endpoint).not_to eq :success + end + end + end + + context "when using the on_failure action" do + let(:actions) { <<-BLOCK } + proc do + on_failure { :fail } + end + BLOCK + + context "when the service fails" do + let(:service) { FailureService } + + it "runs the provided block" do + expect(endpoint).to eq :fail + end + end + + context "when the service does not fail" do + let(:service) { SuccessService } + + it "does not run the provided block" do + expect(endpoint).not_to eq :fail + end + end + end + + context "when using the on_failed_policy action" do + let(:actions) { <<-BLOCK } + proc do + on_failed_policy(:test) { :policy_failure } + end + BLOCK + + context "when the service policy fails" do + let(:service) { FailedPolicyService } + + it "runs the provided block" do + expect(endpoint).to eq :policy_failure + end + end + + context "when the service policy does not fail" do + let(:service) { SuccessPolicyService } + + it "does not run the provided block" do + expect(endpoint).not_to eq :policy_failure + end + end + end + + context "when using the on_failed_contract action" do + let(:actions) { <<-BLOCK } + proc do + on_failed_contract { :contract_failure } + end + BLOCK + + context "when the service contract fails" do + let(:service) { FailedContractService } + + it "runs the provided block" do + expect(endpoint).to eq :contract_failure + end + end + + context "when the service contract does not fail" do + let(:service) { SuccessContractService } + + it "does not run the provided block" do + expect(endpoint).not_to eq :contract_failure + end + end + end + + context "when using the on_model_not_found action" do + let(:actions) { <<-BLOCK } + ->(*) do + on_model_not_found(:fake_model) { :no_model } + end + BLOCK + + context "when the service failed without a model" do + let(:service) { FailureWithModelService } + + it "runs the provided block" do + expect(endpoint).to eq :no_model + end + end + + context "when the service does not fail with a model" do + let(:service) { SuccessWithModelService } + + it "does not run the provided block" do + expect(endpoint).not_to eq :no_model + end + end + end + + context "when using several actions together" do + let(:service) { FailureService } + let(:actions) { <<-BLOCK } + proc do + on_success { :success } + on_failure { :failure } + on_failed_policy { :policy_failure } + end + BLOCK + + it "runs the first matching action" do + expect(endpoint).to eq :failure + end + end + end +end diff --git a/plugins/chat/spec/lib/steps_inspector_spec.rb b/plugins/chat/spec/lib/steps_inspector_spec.rb new file mode 100644 index 00000000000..1c9574bb00e --- /dev/null +++ b/plugins/chat/spec/lib/steps_inspector_spec.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +RSpec.describe Chat::StepsInspector do + class DummyService + include Chat::Service::Base + + model :model + policy :policy + contract + transaction do + step :in_transaction_step_1 + step :in_transaction_step_2 + end + step :final_step + + class Contract + attribute :parameter + + validates :parameter, presence: true + end + end + + subject(:inspector) { described_class.new(result) } + + let(:parameter) { "present" } + let(:result) { DummyService.call(parameter: parameter) } + + before do + class DummyService + %i[fetch_model policy in_transaction_step_1 in_transaction_step_2 final_step].each do |name| + define_method(name) { true } + end + end + end + + describe "#inspect" do + subject(:output) { inspector.inspect } + + context "when service runs without error" do + it "outputs all the steps of the service" do + expect(output).to eq <<~OUTPUT.chomp + [1/7] [model] 'model' ✅ + [2/7] [policy] 'policy' ✅ + [3/7] [contract] 'default' ✅ + [4/7] [transaction] + [5/7] [step] 'in_transaction_step_1' ✅ + [6/7] [step] 'in_transaction_step_2' ✅ + [7/7] [step] 'final_step' ✅ + OUTPUT + end + end + + context "when the model step is failing" do + before do + class DummyService + def fetch_model + false + end + end + end + + it "shows the failing step" do + expect(output).to eq <<~OUTPUT.chomp + [1/7] [model] 'model' ❌ + [2/7] [policy] 'policy' + [3/7] [contract] 'default' + [4/7] [transaction] + [5/7] [step] 'in_transaction_step_1' + [6/7] [step] 'in_transaction_step_2' + [7/7] [step] 'final_step' + OUTPUT + end + end + + context "when the policy step is failing" do + before do + class DummyService + def policy + false + end + end + end + + it "shows the failing step" do + expect(output).to eq <<~OUTPUT.chomp + [1/7] [model] 'model' ✅ + [2/7] [policy] 'policy' ❌ + [3/7] [contract] 'default' + [4/7] [transaction] + [5/7] [step] 'in_transaction_step_1' + [6/7] [step] 'in_transaction_step_2' + [7/7] [step] 'final_step' + OUTPUT + end + end + + context "when the contract step is failing" do + let(:parameter) { nil } + + it "shows the failing step" do + expect(output).to eq <<~OUTPUT.chomp + [1/7] [model] 'model' ✅ + [2/7] [policy] 'policy' ✅ + [3/7] [contract] 'default' ❌ + [4/7] [transaction] + [5/7] [step] 'in_transaction_step_1' + [6/7] [step] 'in_transaction_step_2' + [7/7] [step] 'final_step' + OUTPUT + end + end + + context "when a common step is failing" do + before do + class DummyService + def in_transaction_step_2 + fail!("step error") + end + end + end + + it "shows the failing step" do + expect(output).to eq <<~OUTPUT.chomp + [1/7] [model] 'model' ✅ + [2/7] [policy] 'policy' ✅ + [3/7] [contract] 'default' ✅ + [4/7] [transaction] + [5/7] [step] 'in_transaction_step_1' ✅ + [6/7] [step] 'in_transaction_step_2' ❌ + [7/7] [step] 'final_step' + OUTPUT + end + end + end + + describe "#error" do + subject(:error) { inspector.error } + + context "when there are no errors" do + it "returns nothing" do + expect(error).to be_blank + end + end + + context "when the model step is failing" do + before do + class DummyService + def fetch_model + false + end + end + end + + it "returns an error related to the model" do + expect(error).to match(/Model not found/) + end + end + + context "when the contract step is failing" do + let(:parameter) { nil } + + it "returns an error related to the contract" do + expect(error).to match(/ActiveModel::Error attribute=parameter, type=blank, options={}/) + end + end + + context "when a common step is failing" do + before { result["result.step.final_step"].fail(error: "my error") } + + it "returns an error related to the step" do + expect(error).to eq("my error") + end + end + end +end diff --git a/plugins/chat/spec/plugin_helper.rb b/plugins/chat/spec/plugin_helper.rb index 33657da1fc5..836b8593dcb 100644 --- a/plugins/chat/spec/plugin_helper.rb +++ b/plugins/chat/spec/plugin_helper.rb @@ -22,4 +22,7 @@ module ChatSystemHelpers end end -RSpec.configure { |config| config.include ChatSystemHelpers, type: :system } +RSpec.configure do |config| + config.include ChatSystemHelpers, type: :system + config.include Chat::ServiceMatchers +end diff --git a/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb b/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb index 30955457085..b850a1f841f 100644 --- a/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb +++ b/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb @@ -170,12 +170,7 @@ RSpec.describe Chat::Api::ChatChannelsController do before { sign_in(current_user) } it "returns an error" do - delete "/chat/api/channels/#{channel_1.id}", - params: { - channel: { - name_confirmation: channel_1.title(current_user), - }, - } + delete "/chat/api/channels/#{channel_1.id}" expect(response.status).to eq(403) end @@ -190,38 +185,15 @@ RSpec.describe Chat::Api::ChatChannelsController do before { channel_1.destroy! } it "returns an error" do - delete "/chat/api/channels/#{channel_1.id}", - params: { - channel: { - name_confirmation: channel_1.title(current_user), - }, - } + delete "/chat/api/channels/#{channel_1.id}" expect(response.status).to eq(404) end end - context "when the confirmation doesn’t match the channel name" do - it "returns an error" do - delete "/chat/api/channels/#{channel_1.id}", - params: { - channel: { - name_confirmation: channel_1.title(current_user) + "foo", - }, - } - - expect(response.status).to eq(400) - end - end - context "with valid params" do it "properly destroys the channel" do - delete "/chat/api/channels/#{channel_1.id}", - params: { - channel: { - name_confirmation: channel_1.title(current_user), - }, - } + delete "/chat/api/channels/#{channel_1.id}" expect(response.status).to eq(200) expect(channel_1.reload.trashed?).to eq(true) @@ -243,14 +215,7 @@ RSpec.describe Chat::Api::ChatChannelsController do freeze_time(DateTime.parse("2022-07-08 09:30:00")) old_slug = channel_1.slug - delete( - "/chat/api/channels/#{channel_1.id}", - params: { - channel: { - name_confirmation: channel_1.title(current_user), - }, - }, - ) + delete "/chat/api/channels/#{channel_1.id}" expect(response.status).to eq(200) expect(channel_1.reload.slug).to eq( @@ -371,7 +336,13 @@ RSpec.describe Chat::Api::ChatChannelsController do before { sign_in(Fabricate(:user)) } it "returns a 403" do - put "/chat/api/channels/#{channel.id}" + put "/chat/api/channels/#{channel.id}", + params: { + channel: { + name: "joffrey", + description: "cat owner", + }, + } expect(response.status).to eq(403) end @@ -400,7 +371,7 @@ RSpec.describe Chat::Api::ChatChannelsController do it "nullifies the field and doesn’t store an empty string" do put "/chat/api/channels/#{channel.id}", params: { channel: { name: " " } } - expect(channel.reload.name).to be_nil + expect(channel.reload.name).to eq(nil) end it "doesn’t nullify the description" do @@ -421,7 +392,7 @@ RSpec.describe Chat::Api::ChatChannelsController do it "nullifies the field and doesn’t store an empty string" do put "/chat/api/channels/#{channel.id}", params: { channel: { description: " " } } - expect(channel.reload.description).to be_nil + expect(channel.reload.description).to eq(nil) end it "doesn’t nullify the name" do diff --git a/plugins/chat/spec/services/trash_channel_spec.rb b/plugins/chat/spec/services/trash_channel_spec.rb new file mode 100644 index 00000000000..34e900131d6 --- /dev/null +++ b/plugins/chat/spec/services/trash_channel_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.describe(Chat::Service::TrashChannel) do + subject(:result) { described_class.call(guardian: guardian) } + + let(:guardian) { Guardian.new(current_user) } + + context "when channel_id is not provided" do + fab!(:current_user) { Fabricate(:admin) } + + it { is_expected.to fail_to_find_a_model(:channel) } + end + + context "when channel_id is provided" do + subject(:result) { described_class.call(channel_id: channel.id, guardian: guardian) } + + fab!(:channel) { Fabricate(:chat_channel) } + + context "when user is not allowed to perform the action" do + fab!(:current_user) { Fabricate(:user) } + + it { is_expected.to fail_a_policy(:invalid_access) } + end + + context "when user is allowed to perform the action" do + fab!(:current_user) { Fabricate(:admin) } + + it "sets the service result as successful" do + expect(result).to be_a_success + end + + it "trashes the channel" do + expect(result[:channel]).to be_trashed + end + + it "logs the action" do + expect { result }.to change { UserHistory.count }.by(1) + expect(UserHistory.last).to have_attributes( + custom_type: "chat_channel_delete", + details: + "chat_channel_id: #{result[:channel].id}\nchat_channel_name: #{result[:channel].title(guardian.user)}", + ) + end + + it "changes the slug to prevent colisions" do + expect(result[:channel].slug).to include("deleted") + end + + it "queues a job to delete channel relations" do + expect { result }.to change(Jobs::ChatChannelDelete.jobs, :size).by(1) + end + end + end +end diff --git a/plugins/chat/spec/services/update_channel_spec.rb b/plugins/chat/spec/services/update_channel_spec.rb new file mode 100644 index 00000000000..2627251dd69 --- /dev/null +++ b/plugins/chat/spec/services/update_channel_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +RSpec.describe Chat::Service::UpdateChannel do + subject(:result) { described_class.call(guardian: guardian, channel_id: channel.id, **params) } + + fab!(:channel) { Fabricate(:chat_channel) } + fab!(:current_user) { Fabricate(:admin) } + + let(:guardian) { Guardian.new(current_user) } + let(:params) do + { + name: "cool channel", + description: "a channel description", + slug: "snail", + allow_channel_wide_mentions: true, + auto_join_users: false, + } + end + + context "when the user cannot edit the channel" do + fab!(:current_user) { Fabricate(:user) } + + it { is_expected.to fail_a_policy(:check_channel_permission) } + end + + context "when the user tries to edit a DM channel" do + fab!(:channel) { Fabricate(:direct_message_channel, users: [current_user, Fabricate(:user)]) } + + it { is_expected.to fail_a_policy(:no_direct_message_channel) } + end + + context "when channel is a category one" do + context "when a valid user provides valid params" do + let(:message) do + MessageBus.track_publish(ChatPublisher::CHANNEL_EDITS_MESSAGE_BUS_CHANNEL) { result }.first + end + + it "sets the service result as successful" do + expect(result).to be_a_success + end + + it "updates the channel accordingly" do + result + expect(channel.reload).to have_attributes( + name: "cool channel", + slug: "snail", + description: "a channel description", + allow_channel_wide_mentions: true, + auto_join_users: false, + ) + end + + it "publishes a MessageBus message" do + expect(message.data).to eq( + { + chat_channel_id: channel.id, + name: "cool channel", + description: "a channel description", + slug: "snail", + }, + ) + end + + context "when the name is blank" do + before { params[:name] = "" } + + it "nils out the name" do + result + expect(channel.reload.name).to be_nil + end + end + + context "when the description is blank" do + before do + channel.update!(description: "something") + params[:description] = "" + end + + it "nils out the description" do + result + expect(channel.reload.description).to be_nil + end + end + + context "when auto_join_users is set to 'true'" do + before do + channel.update!(auto_join_users: false) + params[:auto_join_users] = true + end + + it "updates the model accordingly" do + result + expect(channel.reload).to have_attributes(auto_join_users: true) + end + + it "auto joins users" do + expect_enqueued_with( + job: :auto_manage_channel_memberships, + args: { + chat_channel_id: channel.id, + }, + ) { result } + end + end + end + end +end diff --git a/plugins/chat/spec/services/update_channel_status_spec.rb b/plugins/chat/spec/services/update_channel_status_spec.rb new file mode 100644 index 00000000000..54c42b1694b --- /dev/null +++ b/plugins/chat/spec/services/update_channel_status_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.describe(Chat::Service::UpdateChannelStatus) do + subject(:result) do + described_class.call(guardian: guardian, channel_id: channel.id, status: status) + end + + fab!(:channel) { Fabricate(:chat_channel) } + fab!(:current_user) { Fabricate(:admin) } + + let(:guardian) { Guardian.new(current_user) } + let(:status) { "open" } + + context "when no channel_id is given" do + subject(:result) { described_class.call(guardian: guardian, status: status) } + + it { is_expected.to fail_to_find_a_model(:channel) } + end + + context "when user is not allowed to change channel status" do + fab!(:current_user) { Fabricate(:user) } + + it { is_expected.to fail_a_policy(:check_channel_permission) } + end + + context "when status is not allowed" do + (ChatChannel.statuses.keys - ChatChannel.editable_statuses.keys).each do |na_status| + context "when status is '#{na_status}'" do + let(:status) { na_status } + + it { is_expected.to fail_a_contract } + end + end + end + + context "when new status is the same than the existing one" do + let(:status) { channel.status } + + it { is_expected.to fail_a_policy(:check_channel_permission) } + end + + context "when status is allowed" do + let(:status) { "closed" } + + it "sets the service result as successful" do + expect(result).to be_a_success + end + + it "changes the status" do + result + expect(channel.reload).to be_closed + end + end +end diff --git a/plugins/chat/spec/services/update_user_last_read_spec.rb b/plugins/chat/spec/services/update_user_last_read_spec.rb new file mode 100644 index 00000000000..4926e824f6c --- /dev/null +++ b/plugins/chat/spec/services/update_user_last_read_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +RSpec.describe(Chat::Service::UpdateUserLastRead) do + subject(:result) { described_class.call(params) } + + fab!(:current_user) { Fabricate(:user) } + fab!(:channel) { Fabricate(:chat_channel) } + fab!(:membership) do + Fabricate(:user_chat_channel_membership, user: current_user, chat_channel: channel) + end + fab!(:message_1) { Fabricate(:chat_message, chat_channel: membership.chat_channel) } + + let(:guardian) { Guardian.new(current_user) } + let(:params) do + { + guardian: guardian, + user_id: current_user.id, + channel_id: channel.id, + message_id: message_1.id, + } + end + + context "when channel_id is not provided" do + before { params.delete(:channel_id) } + + it { is_expected.to fail_to_find_a_model(:membership) } + end + + context "when user_id is not provided" do + before { params.delete(:user_id) } + + it { is_expected.to fail_to_find_a_model(:membership) } + end + + context "when user has no membership" do + before { membership.destroy! } + + it { is_expected.to fail_to_find_a_model(:membership) } + end + + context "when user can’t access the channel" do + fab!(:membership) do + Fabricate( + :user_chat_channel_membership, + user: current_user, + chat_channel: Fabricate(:private_category_channel), + ) + end + + before { params[:channel_id] = membership.chat_channel.id } + + it { is_expected.to fail_a_policy(:invalid_access) } + end + + context "when message_id is older than membership's last_read_message_id" do + before do + params[:message_id] = -2 + membership.update!(last_read_message_id: -1) + end + + it { is_expected.to fail_a_policy(:ensure_message_id_recency) } + end + + context "when message doesn’t exist" do + before do + params[:message_id] = 2 + membership.update!(last_read_message_id: 1) + end + + it { is_expected.to fail_a_policy(:ensure_message_exists) } + end + + context "when params are valid" do + before { Jobs.run_immediately! } + + it "sets the service result as successful" do + expect(result).to be_a_success + end + + it "updates the last_read message id" do + expect { result }.to change { membership.reload.last_read_message_id }.to(message_1.id) + end + + it "marks existing notifications related to the message as read" do + expect { + notification = + Fabricate( + :notification, + notification_type: Notification.types[:chat_mention], + user: current_user, + ) + + # FIXME: we need a better way to create proper chat mention + ChatMention.create!(notification: notification, user: current_user, chat_message: message_1) + }.to change { + Notification.where( + notification_type: Notification.types[:chat_mention], + user: current_user, + read: false, + ).count + }.by(1) + + expect { result }.to change { + Notification.where( + notification_type: Notification.types[:chat_mention], + user: current_user, + read: false, + ).count + }.by(-1) + end + + it "publishes new last read to clients" do + messages = MessageBus.track_publish { result } + + expect(messages.map(&:channel)).to include("/chat/user-tracking-state/#{current_user.id}") + end + end +end diff --git a/plugins/chat/spec/support/chat_service_matcher.rb b/plugins/chat/spec/support/chat_service_matcher.rb new file mode 100644 index 00000000000..bdd2649bbd1 --- /dev/null +++ b/plugins/chat/spec/support/chat_service_matcher.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +module Chat + module ServiceMatchers + class FailStep + attr_reader :name, :result + + def initialize(name) + @name = name + end + + def matches?(result) + @result = result + step_exists? && step_failed? && service_failed? + end + + def failure_message + message = + if !step_exists? + "Expected #{type} '#{name}' (key: '#{step}') was not found in the result object." + elsif !step_failed? + "Expected #{type} '#{name}' (key: '#{step}') to fail but it succeeded." + else + "expected the service to fail but it succeeded." + end + error_message_with_inspection(message) + end + + def failure_message_when_negated + message = "Expected #{type} '#{name}' (key: '#{step}') to succeed but it failed." + error_message_with_inspection(message) + end + + private + + def step_exists? + result[step].present? + end + + def step_failed? + result[step].failure? + end + + def service_failed? + result.failure? + end + + def type + "step" + end + + def error_message_with_inspection(message) + inspector = StepsInspector.new(result) + "#{message}\n\n#{inspector.inspect}\n\n#{inspector.error}" + end + end + + class FailContract < FailStep + attr_reader :error_message + + def step + "result.contract.#{name}" + end + + def type + "contract" + end + + def matches?(service) + super && has_error? + end + + def has_error? + result[step].errors.present? + end + + def failure_message + return "expected contract '#{step}' to have errors" unless has_error? + super + end + + def description + "fail a contract named '#{name}'" + end + end + + class FailPolicy < FailStep + def type + "policy" + end + + def step + "result.policy.#{name}" + end + + def description + "fail a policy named '#{name}'" + end + end + + class FailToFindModel < FailStep + def type + "model" + end + + def step + "result.model.#{name}" + end + + def description + "fail to find a model named '#{name}'" + end + end + + def fail_a_policy(name) + FailPolicy.new(name) + end + + def fail_a_contract(name = "default") + FailContract.new(name) + end + + def fail_to_find_a_model(name = "model") + FailToFindModel.new(name) + end + + def inspect_steps(result) + inspector = Chat::StepsInspector.new(result) + puts "Steps:" + puts inspector.inspect + puts "\nFirst error:" + puts inspector.error + end + end +end diff --git a/yarn.lock b/yarn.lock index 402409e17cc..ac72c888b5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -567,13 +567,6 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-escape-sequences@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz#2483c8773f50dd9174dd9557e92b1718f1816097" - integrity sha512-dzW9kHxH011uBsidTXd14JXgzye/YLb2LzeKZ4bsgl/Knwx8AtbSFkkGxagdNOoh0DlqHCmfiEjWKBaqjOanVw== - dependencies: - array-back "^3.0.1" - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -604,40 +597,6 @@ aria-query@^5.0.0: resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== -array-back@^1.0.2, array-back@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-1.0.4.tgz#644ba7f095f7ffcf7c43b5f0dc39d3c1f03c063b" - integrity sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw== - dependencies: - typical "^2.6.0" - -array-back@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-2.0.0.tgz#6877471d51ecc9c9bfa6136fb6c7d5fe69748022" - integrity sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw== - dependencies: - typical "^2.6.1" - -array-back@^3.0.1, array-back@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" - integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== - -array-back@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" - integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== - -array-back@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-5.0.0.tgz#e196609edcec48376236d163958df76e659a0d36" - integrity sha512-kgVWwJReZWmVuWOQKEOohXKJX+nD02JAZ54D1RRWlv8L0NebauKAaFxACKzB74RTclt1+WNz5KHaLRDAPZbDEw== - -array-back@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-6.2.2.tgz#f567d99e9af88a6d3d2f9dfcc21db6f9ba9fd157" - integrity sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw== - array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" @@ -725,15 +684,6 @@ buffer@^5.2.1, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -cache-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cache-point/-/cache-point-2.0.0.tgz#91e03c38da9cfba9d95ac6a34d24cfe6eff8920f" - integrity sha512-4gkeHlFpSKgm3vm2gJN5sPqfmijYRFYCQ6tv5cLw0xVmT6r1z1vd4FNnpuOREco3cBs1G709sZ72LdgddKvL5w== - dependencies: - array-back "^4.0.1" - fs-then-native "^2.0.0" - mkdirp2 "^1.0.4" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -832,14 +782,6 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= -collect-all@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/collect-all/-/collect-all-1.0.4.tgz#50cd7119ac24b8e12a661f0f8c3aa0ea7222ddfc" - integrity sha512-RKZhRwJtJEP5FWul+gkSMEnaK6H3AGPTTWOiRimCcs+rc/OmQE3Yhy1Q7A7KsdkG3ZXVdZq68Y6ONSdvkeEcKA== - dependencies: - stream-connect "^1.0.2" - stream-via "^1.0.4" - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -869,37 +811,6 @@ colors@^1.4.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -command-line-args@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" - integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== - dependencies: - array-back "^3.1.0" - find-replace "^3.0.0" - lodash.camelcase "^4.3.0" - typical "^4.0.0" - -command-line-tool@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/command-line-tool/-/command-line-tool-0.8.0.tgz#b00290ef1dfc11cc731dd1f43a92cfa5f21e715b" - integrity sha512-Xw18HVx/QzQV3Sc5k1vy3kgtOeGmsKIqwtFFoyjI4bbcpSgnw2CWVULvtakyw4s6fhyAdI6soQQhXc2OzJy62g== - dependencies: - ansi-escape-sequences "^4.0.0" - array-back "^2.0.0" - command-line-args "^5.0.0" - command-line-usage "^4.1.0" - typical "^2.6.1" - -command-line-usage@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-4.1.0.tgz#a6b3b2e2703b4dcf8bd46ae19e118a9a52972882" - integrity sha512-MxS8Ad995KpdAC0Jopo/ovGIroV/m0KHwzKfXxKag6FHOkGsH8/lv5yjgablcRxCJJC0oJeUMuO/gmaq+Wq46g== - dependencies: - ansi-escape-sequences "^4.0.0" - array-back "^2.0.0" - table-layout "^0.4.2" - typical "^2.6.1" - commander@2.11.x: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" @@ -915,23 +826,11 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -common-sequence@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/common-sequence/-/common-sequence-2.0.2.tgz#accc76bdc5876a1fcd92b73484d4285fff99d838" - integrity sha512-jAg09gkdkrDO9EWTdXfv80WWH3yeZl5oT69fGfedBNS9pXUKYInVJ1bJ+/ht2+Moeei48TmSbQDYMc8EOx9G0g== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -config-master@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/config-master/-/config-master-3.1.0.tgz#667663590505a283bf26a484d68489d74c5485da" - integrity sha512-n7LBL1zBzYdTpF1mx5DNcZnZn05CWIdsdvtPL4MosvqbBUK3Rq6VWEtGUuF3Y0s9/CIhMejezqlSkP6TnCJ/9g== - dependencies: - walk-back "^2.0.1" - convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" @@ -992,11 +891,6 @@ debug@^2.6.8: dependencies: ms "2.0.0" -deep-extend@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -1026,24 +920,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dmd@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/dmd/-/dmd-6.2.0.tgz#d267a9fb1ce62b74edca8bf5bcbd3b8e08574fe7" - integrity sha512-uXWxLF1H7TkUAuoHK59/h/ts5cKavm2LnhrIgJWisip4BVzPoXavlwyoprFFn2CzcahKYgvkfaebS6oxzgflkg== - dependencies: - array-back "^6.2.2" - cache-point "^2.0.0" - common-sequence "^2.0.2" - file-set "^4.0.2" - handlebars "^4.7.7" - marked "^4.2.3" - object-get "^2.1.1" - reduce-flatten "^3.0.1" - reduce-unique "^2.0.1" - reduce-without "^1.0.1" - test-value "^3.0.0" - walk-back "^5.1.0" - doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -1416,14 +1292,6 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-set@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/file-set/-/file-set-4.0.2.tgz#8d67c92a864202c2085ac9f03f1c9909c7e27030" - integrity sha512-fuxEgzk4L8waGXaAkd8cMr73Pm0FxOVkn8hztzUW7BAHhOGH90viQNXbiOsnecCWmfInqU6YmAMwxRMdKETceQ== - dependencies: - array-back "^5.0.0" - glob "^7.1.6" - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -1431,13 +1299,6 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-replace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" - integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== - dependencies: - array-back "^3.0.1" - find-up@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -1490,11 +1351,6 @@ fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-then-native@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fs-then-native/-/fs-then-native-2.0.0.tgz#19a124d94d90c22c8e045f2e8dd6ebea36d48c67" - integrity sha512-X712jAOaWXkemQCAmWeg5rOT2i+KOpWz1Z/txk/cW0qlOu2oQ9H61vc5w3X/iyuUEfq/OyaFJ78/cZAQD1/bgA== - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1567,7 +1423,7 @@ glob-stream@^7.0.0: to-absolute-glob "^2.0.2" unique-stream "^2.3.1" -glob@^7.1.3, glob@^7.1.6, glob@^7.2.0: +glob@^7.1.3, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1619,18 +1475,6 @@ graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -handlebars@^4.7.7: - version "4.7.7" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" - integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== - dependencies: - minimist "^1.2.5" - neo-async "^2.6.0" - source-map "^0.6.1" - wordwrap "^1.0.0" - optionalDependencies: - uglify-js "^3.1.4" - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -1692,6 +1536,11 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + is-absolute@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" @@ -1804,45 +1653,26 @@ js2xmlparser@^4.0.2: dependencies: xmlcreate "^2.0.4" -jsdoc-api@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/jsdoc-api/-/jsdoc-api-8.0.0.tgz#4b2c25ff60f91b80da51b6cd33943acc7b2cab74" - integrity sha512-Rnhor0suB1Ds1abjmFkFfKeD+kSMRN9oHMTMZoJVUrmtCGDwXty+sWMA9sa4xbe4UyxuPjhC7tavZ40mDKK6QQ== +jsdoc@^3.6.3: + version "3.6.11" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.6.11.tgz#8bbb5747e6f579f141a5238cbad4e95e004458ce" + integrity sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg== dependencies: - array-back "^6.2.2" - cache-point "^2.0.0" - collect-all "^1.0.4" - file-set "^4.0.2" - fs-then-native "^2.0.0" - jsdoc "^4.0.0" - object-to-spawn-args "^2.0.1" - temp-path "^1.0.0" - walk-back "^5.1.0" - -jsdoc-parse@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/jsdoc-parse/-/jsdoc-parse-6.2.0.tgz#2b71d3925acfc4badc72526f2470766e0561f6b5" - integrity sha512-Afu1fQBEb7QHt6QWX/6eUWvYHJofB90Fjx7FuJYF7mnG9z5BkAIpms1wsnvYLytfmqpEENHs/fax9p8gvMj7dw== - dependencies: - array-back "^6.2.2" - lodash.omit "^4.5.0" - lodash.pick "^4.4.0" - reduce-extract "^1.0.0" - sort-array "^4.1.5" - test-value "^3.0.0" - -jsdoc-to-markdown@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/jsdoc-to-markdown/-/jsdoc-to-markdown-8.0.0.tgz#27f32ed200d3b84dbf22a49beed485790f93b3ce" - integrity sha512-2FQvYkg491+FP6s15eFlgSSWs69CvQrpbABGYBtvAvGWy/lWo8IKKToarT283w59rQFrpcjHl3YdhHCa3l7gXg== - dependencies: - array-back "^6.2.2" - command-line-tool "^0.8.0" - config-master "^3.1.0" - dmd "^6.2.0" - jsdoc-api "^8.0.0" - jsdoc-parse "^6.2.0" - walk-back "^5.1.0" + "@babel/parser" "^7.9.4" + "@types/markdown-it" "^12.2.3" + bluebird "^3.7.2" + catharsis "^0.9.0" + escape-string-regexp "^2.0.0" + js2xmlparser "^4.0.2" + klaw "^3.0.0" + markdown-it "^12.3.2" + markdown-it-anchor "^8.4.1" + marked "^4.0.10" + mkdirp "^1.0.4" + requizzle "^0.2.3" + strip-json-comments "^3.1.0" + taffydb "2.6.2" + underscore "~1.13.2" jsdoc@^4.0.0: version "4.0.0" @@ -1999,11 +1829,6 @@ locate-path@^7.1.0: dependencies: p-locate "^6.0.0" -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== - lodash.kebabcase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -2014,21 +1839,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.omit@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" - integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg== - -lodash.padend@^4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" - integrity sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw== - -lodash.pick@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== - lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -2070,7 +1880,7 @@ markdown-it@^12.3.2: mdurl "^1.0.1" uc.micro "^1.0.5" -marked@^4.0.10, marked@^4.2.3: +marked@^4.0.10: version "4.2.5" resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.5.tgz#979813dfc1252cc123a79b71b095759a32f42a5d" integrity sha512-jPueVhumq7idETHkb203WDD4fMA3yV9emQ5vLwop58lu8bTclMghBWcYAavlDqIEMaisADinV1TooIFCfqOsYQ== @@ -2115,21 +1925,11 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.5: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== - mkdirp-classic@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp2@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/mkdirp2/-/mkdirp2-1.0.5.tgz#68bbe61defefafce4b48948608ec0bac942512c2" - integrity sha512-xOE9xbICroUDmG1ye2h4bZ8WBie9EGmACaco8K8cx6RlkJJrxGIqjGqztAI+NMhexXBcdGbSEzI6N3EJPevxZw== - mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -2162,11 +1962,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -neo-async@^2.6.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -2187,16 +1982,6 @@ node-releases@^2.0.5: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== -object-get@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/object-get/-/object-get-2.1.1.tgz#1dad63baf6d94df184d1c58756cc9be55b174dac" - integrity sha512-7n4IpLMzGGcLEMiQKsNR7vCe+N5E9LORFrtNUVy4sO3dj9a3HedZCxEL2T7QuLhcHN1NBuBsMOKaOsAYI9IIvg== - -object-to-spawn-args@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz#cf8b8e3c9b3589137a469cac90391f44870144a5" - integrity sha512-6FuKFQ39cOID+BMZ3QaphcC8Y4cw6LXBLyIgPU+OhIYwviJamPAn+4mITapnSBQrejB+NNp+FMskhD8Cq+Ys3w== - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2452,35 +2237,6 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" -reduce-extract@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/reduce-extract/-/reduce-extract-1.0.0.tgz#67f2385beda65061b5f5f4312662e8b080ca1525" - integrity sha512-QF8vjWx3wnRSL5uFMyCjDeDc5EBMiryoT9tz94VvgjKfzecHAVnqmXAwQDcr7X4JmLc2cjkjFGCVzhMqDjgR9g== - dependencies: - test-value "^1.0.1" - -reduce-flatten@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327" - integrity sha512-j5WfFJfc9CoXv/WbwVLHq74i/hdTUpy+iNC534LxczMRP67vJeK3V9JOdnL0N1cIRbn9mYhE2yVjvvKXDxvNXQ== - -reduce-flatten@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-3.0.1.tgz#3db6b48ced1f4dbe4f4f5e31e422aa9ff0cd21ba" - integrity sha512-bYo+97BmUUOzg09XwfkwALt4PQH1M5L0wzKerBt6WLm3Fhdd43mMS89HiT1B9pJIqko/6lWx3OnV4J9f2Kqp5Q== - -reduce-unique@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/reduce-unique/-/reduce-unique-2.0.1.tgz#fb34b90e89297c1e08d75dcf17e9a6443ea71081" - integrity sha512-x4jH/8L1eyZGR785WY+ePtyMNhycl1N2XOLxhCbzZFaqF4AXjLzqSxa2UHgJ2ZVR/HHyPOvl1L7xRnW8ye5MdA== - -reduce-without@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/reduce-without/-/reduce-without-1.0.1.tgz#68ad0ead11855c9a37d4e8256c15bbf87972fc8c" - integrity sha512-zQv5y/cf85sxvdrKPlfcRzlDn/OqKFThNimYmsS3flmkioKvkUGn2Qg9cJVoQiEvdxFGLE0MQER/9fZ9sUqdxg== - dependencies: - test-value "^2.0.0" - regexpp@^3.0.0, regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -2619,47 +2375,22 @@ snake-case@^3.0.3: dot-case "^3.0.4" tslib "^2.0.3" -sort-array@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/sort-array/-/sort-array-4.1.5.tgz#64b92aaba222aec606786f4df28ae4e3e3e68313" - integrity sha512-Ya4peoS1fgFN42RN1REk2FgdNOeLIEMKFGJvs7VTP3OklF8+kl2SkpVliZ4tk/PurWsrWRsdNdU+tgyOBkB9sA== - dependencies: - array-back "^5.0.0" - typical "^6.0.1" - source-map-js@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - squoosh@discourse/squoosh#dc9649d: version "2.0.0" resolved "https://codeload.github.com/discourse/squoosh/tar.gz/dc9649d0a4d396d1251c22291b17d99f1716da44" dependencies: wasm-feature-detect "^1.2.11" -stream-connect@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/stream-connect/-/stream-connect-1.0.2.tgz#18bc81f2edb35b8b5d9a8009200a985314428a97" - integrity sha512-68Kl+79cE0RGKemKkhxTSg8+6AGrqBt+cbZAXevg2iJ6Y3zX4JhA/sZeGzLpxW9cXhmqAcE7KnJCisUmIUfnFQ== - dependencies: - array-back "^1.0.2" - stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== -stream-via@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/stream-via/-/stream-via-1.0.4.tgz#8dccbb0ac909328eb8bc8e2a4bd3934afdaf606c" - integrity sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ== - string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -2714,16 +2445,15 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -table-layout@^0.4.2: - version "0.4.5" - resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.5.tgz#d906de6a25fa09c0c90d1d08ecd833ecedcb7378" - integrity sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw== - dependencies: - array-back "^2.0.0" - deep-extend "~0.6.0" - lodash.padend "^4.6.1" - typical "^2.6.1" - wordwrapjs "^3.0.0" +taffydb@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" + integrity sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA== + +taffydb@^2.7.3: + version "2.7.3" + resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.7.3.tgz#2ad37169629498fca5bc84243096d3cde0ec3a34" + integrity sha512-GQ3gtYFSOAxSMN/apGtDKKkbJf+8izz5YfbGqIsUc7AMiQOapARZ76dhilRY2h39cynYxBFdafQo5HUL5vgkrg== tar-fs@2.1.1: version "2.1.1" @@ -2746,35 +2476,6 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -temp-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/temp-path/-/temp-path-1.0.0.tgz#24b1543973ab442896d9ad367dd9cbdbfafe918b" - integrity sha512-TvmyH7kC6ZVTYkqCODjJIbgvu0FKiwQpZ4D1aknE7xpcDf/qEOB8KZEK5ef2pfbVoiBhNWs3yx4y+ESMtNYmlg== - -test-value@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/test-value/-/test-value-1.1.0.tgz#a09136f72ec043d27c893707c2b159bfad7de93f" - integrity sha512-wrsbRo7qP+2Je8x8DsK8ovCGyxe3sYfQwOraIY/09A2gFXU9DYKiTF14W4ki/01AEh56kMzAmlj9CaHGDDUBJA== - dependencies: - array-back "^1.0.2" - typical "^2.4.2" - -test-value@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/test-value/-/test-value-2.1.0.tgz#11da6ff670f3471a73b625ca4f3fdcf7bb748291" - integrity sha512-+1epbAxtKeXttkGFMTX9H42oqzOTufR1ceCF+GYA5aOmvaPq9wd4PUS8329fn2RRLGNeUkgRLnVpycjx8DsO2w== - dependencies: - array-back "^1.0.3" - typical "^2.6.0" - -test-value@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/test-value/-/test-value-3.0.0.tgz#9168c062fab11a86b8d444dd968bb4b73851ce92" - integrity sha512-sVACdAWcZkSU9x7AOmJo5TqE+GyNJknHaHsMrR6ZnhjVlVN9Yx6FjHrsKZ3BjIpPCT68zYesPWkakrNupwfOTQ== - dependencies: - array-back "^2.0.0" - typical "^2.6.1" - text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -2801,6 +2502,15 @@ through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tidy-jsdoc@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/tidy-jsdoc/-/tidy-jsdoc-1.4.1.tgz#609289afb4094c4b4cb4367cbce746f940232edd" + integrity sha512-FpH1oL6fEMMO0qPPAjoV8peAriwTjdys92TMsfMufrDERDGfmg2w90ieqOQ4RGDH7yuvDTqxR7a0W1Mfun8fzA== + dependencies: + jsdoc "^3.6.3" + taffydb "^2.7.3" + util "^0.10.3" + tmp@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -2855,31 +2565,11 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typical@^2.4.2, typical@^2.6.0, typical@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" - integrity sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg== - -typical@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" - integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== - -typical@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/typical/-/typical-6.0.1.tgz#89bd1a6aa5e5e96fa907fb6b7579223bff558a06" - integrity sha512-+g3NEp7fJLe9DPa1TArHm9QAA7YciZmWnfAqEaFrBihQ7epOv9i99rjtgb6Iz0wh3WuQDjsCTDfgRoGnmHN81A== - uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== -uglify-js@^3.1.4: - version "3.17.4" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" - integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== - unbzip2-stream@1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" @@ -2936,21 +2626,18 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -walk-back@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-2.0.1.tgz#554e2a9d874fac47a8cb006bf44c2f0c4998a0a4" - integrity sha512-Nb6GvBR8UWX1D+Le+xUq0+Q1kFmRBIWVrfLnQAOmcpEzA9oAxwJ9gIr36t9TWYfzvWRvuMtjHiVsJYEkXWaTAQ== - -walk-back@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-5.1.0.tgz#486d6f29e67f56ab89b952d987028bbb1a4e956c" - integrity sha512-Uhxps5yZcVNbLEAnb+xaEEMdgTXl9qAQDzKYejG2AZ7qPwRQ81lozY9ECDbjLPNWm7YsO1IK5rsP1KoQzXAcGA== - wasm-feature-detect@^1.2.11: version "1.3.0" resolved "https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.3.0.tgz#fb3fc5dd4a1ba950a429be843daad67fe048bc42" @@ -2988,19 +2675,6 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wordwrap@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== - -wordwrapjs@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-3.0.0.tgz#c94c372894cadc6feb1a66bff64e1d9af92c5d1e" - integrity sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw== - dependencies: - reduce-flatten "^1.0.1" - typical "^2.6.1" - workbox-cacheable-response@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-4.3.1.tgz#f53e079179c095a3f19e5313b284975c91428c91"