diff --git a/plugins/chat/app/controllers/api/chat_threads_controller.rb b/plugins/chat/app/controllers/api/chat_threads_controller.rb
new file mode 100644
index 0000000000..d90336fe30
--- /dev/null
+++ b/plugins/chat/app/controllers/api/chat_threads_controller.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Chat::Api::ChatThreadsController < Chat::Api
+ def show
+ render_serialized(
+ ChatThread.includes(:original_message, :original_message_user).find(params[:thread_id]),
+ ChatThreadSerializer,
+ root: "thread",
+ )
+ end
+end
diff --git a/plugins/chat/app/models/chat_channel.rb b/plugins/chat/app/models/chat_channel.rb
index c7ee81ea35..6d8869426c 100644
--- a/plugins/chat/app/models/chat_channel.rb
+++ b/plugins/chat/app/models/chat_channel.rb
@@ -9,6 +9,7 @@ class ChatChannel < ActiveRecord::Base
foreign_key: "chatable_id"
has_many :chat_messages
+ has_many :threads, class_name: "ChatThread", foreign_key: :channel_id
has_many :user_chat_channel_memberships
has_one :chat_channel_archive
diff --git a/plugins/chat/app/serializers/chat_channel_serializer.rb b/plugins/chat/app/serializers/chat_channel_serializer.rb
index e6707acfd6..c163b47737 100644
--- a/plugins/chat/app/serializers/chat_channel_serializer.rb
+++ b/plugins/chat/app/serializers/chat_channel_serializer.rb
@@ -20,7 +20,8 @@ class ChatChannelSerializer < ApplicationSerializer
:archive_topic_id,
:memberships_count,
:current_user_membership,
- :meta
+ :meta,
+ :threading_enabled
def initialize(object, opts)
super(object, opts)
@@ -115,6 +116,10 @@ class ChatChannelSerializer < ApplicationSerializer
}
end
+ def threading_enabled
+ SiteSetting.enable_experimental_chat_threaded_discussions && object.threading_enabled
+ end
+
alias_method :include_archive_topic_id?, :include_archive_status?
alias_method :include_total_messages?, :include_archive_status?
alias_method :include_archived_messages?, :include_archive_status?
diff --git a/plugins/chat/app/serializers/chat_message_serializer.rb b/plugins/chat/app/serializers/chat_message_serializer.rb
index 0bcbd64c3d..f24b3aa403 100644
--- a/plugins/chat/app/serializers/chat_message_serializer.rb
+++ b/plugins/chat/app/serializers/chat_message_serializer.rb
@@ -13,7 +13,8 @@ class ChatMessageSerializer < ApplicationSerializer
:edited,
:reactions,
:bookmark,
- :available_flags
+ :available_flags,
+ :thread_id
has_one :user, serializer: BasicUserWithStatusSerializer, embed: :objects
has_one :chat_webhook_event, serializer: ChatWebhookEventSerializer, embed: :objects
diff --git a/plugins/chat/app/serializers/chat_thread_serializer.rb b/plugins/chat/app/serializers/chat_thread_serializer.rb
new file mode 100644
index 0000000000..06ad43b28a
--- /dev/null
+++ b/plugins/chat/app/serializers/chat_thread_serializer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class ChatThreadSerializer < ApplicationSerializer
+ has_one :original_message_user, serializer: BasicUserSerializer, embed: :objects
+
+ attributes :id, :title, :status, :original_message_id, :original_message_excerpt, :created_at
+
+ def original_message_excerpt
+ object.original_message.excerpt
+ end
+end
diff --git a/plugins/chat/app/services/chat_publisher.rb b/plugins/chat/app/services/chat_publisher.rb
index e02a52328a..a56b841ae9 100644
--- a/plugins/chat/app/services/chat_publisher.rb
+++ b/plugins/chat/app/services/chat_publisher.rb
@@ -24,6 +24,7 @@ module ChatPublisher
message_id: chat_message.id,
user_id: chat_message.user.id,
username: chat_message.user.username,
+ thread_id: chat_message.thread_id,
},
permissions,
)
diff --git a/plugins/chat/assets/javascripts/discourse/chat-route-map.js b/plugins/chat/assets/javascripts/discourse/chat-route-map.js
index d303778eae..c2745e08e9 100644
--- a/plugins/chat/assets/javascripts/discourse/chat-route-map.js
+++ b/plugins/chat/assets/javascripts/discourse/chat-route-map.js
@@ -14,6 +14,8 @@ export default function () {
this.route("members", { path: "/members" });
this.route("settings", { path: "/settings" });
});
+
+ this.route("thread", { path: "/t/:threadId" });
});
this.route("draft-channel", { path: "/draft-channel" });
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs
index a6a17db58f..33d204a729 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs
@@ -152,4 +152,4 @@
{{else}}
{{/if}}
-{{/if}}
\ No newline at end of file
+{{/if}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js
index 42fdf41345..c832920140 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js
@@ -789,6 +789,7 @@ export default Component.extend({
id: data.chat_message.id,
staged_id: null,
excerpt: data.chat_message.excerpt,
+ thread_id: data.chat_message.thread_id,
});
// some markdown is cooked differently on the server-side, e.g.
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-mention-warnings.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-mention-warnings.hbs
index 104a4bb92d..a224e358aa 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-mention-warnings.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-mention-warnings.hbs
@@ -21,4 +21,4 @@
-{{/if}}
\ No newline at end of file
+{{/if}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-info.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message-info.hbs
index a6e06bb6e7..cb2187a921 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message-info.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-info.hbs
@@ -28,6 +28,13 @@
{{format-chat-date @message @details}}
+ {{!-- TODO (martin): Remove this before merge. --}}
+ {{#if @message.thread_id}}
+
+ THREAD ID: {{@message.thread_id}}
+
+ {{/if}}
+
{{#if @message.bookmark}}
@@ -45,4 +52,4 @@
{{/if}}
{{/if}}
-
\ No newline at end of file
+
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs
index c673929683..d972db6e80 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs
@@ -82,9 +82,10 @@
{{else}}
{{#if this.message.in_reply_to}}
+ {{!-- TOOD: Maybe split this into ChatMessageReplyTo component? --}}
{{d-icon "share" title="chat.in_reply_to"}}
@@ -227,4 +228,4 @@
{{/if}}
{{/if}}
-
\ No newline at end of file
+
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.js b/plugins/chat/assets/javascripts/discourse/components/chat-message.js
index 6e89eee173..c7d901ad7c 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.js
@@ -49,6 +49,7 @@ export default Component.extend({
tagName: "",
chat: service(),
dialog: service(),
+ router: service(),
chatMessageActionsMobileAnchor: null,
chatMessageActionsDesktopAnchor: null,
chatMessageEmojiPickerAnchor: null,
@@ -678,8 +679,15 @@ export default Component.extend({
},
@action
- viewReply() {
- this.replyMessageClicked(this.message.in_reply_to);
+ viewReplyOrThread() {
+ // TODO (martin) Clean this up, hack
+ if (this.chatChannel.threading_enabled) {
+ return this.router.transitionTo("chat.channel.thread", {
+ threadId: this.message.thread_id,
+ });
+ } else {
+ this.replyMessageClicked(this.message.in_reply_to);
+ }
},
@action
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-thread-pane.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-thread-pane.hbs
new file mode 100644
index 0000000000..5633d3016a
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-thread-pane.hbs
@@ -0,0 +1,10 @@
+
+
Thread ID {{@thread.id}}, started by {{@thread.original_message_user.username}}
+
+
Excerpt: {{@thread.original_message_excerpt}}
+
+
Close thread
+
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-thread-pane.js b/plugins/chat/assets/javascripts/discourse/components/chat-thread-pane.js
new file mode 100644
index 0000000000..2dcf8c07ba
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-thread-pane.js
@@ -0,0 +1,20 @@
+import Component from "@glimmer/component";
+import { action } from "@ember/object";
+import { inject as service } from "@ember/service";
+
+export default class ChatThreadPane extends Component {
+ @service siteSettings;
+ @service currentUser;
+ @service chat;
+ @service router;
+
+ channel = null;
+ thread = null;
+
+ @action
+ closeThread() {
+ return this.router.transitionTo("chat.channel", {
+ channelId: this.args.channel.id,
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs
index eb443dabcd..b20ebde372 100644
--- a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs
@@ -1,7 +1,12 @@
{{#if this.chat.activeChannel}}
-{{/if}}
\ No newline at end of file
+
+{{/if}}
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-thread.js b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
new file mode 100644
index 0000000000..eb176e5e72
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
@@ -0,0 +1,77 @@
+import RestModel from "discourse/models/rest";
+import I18n from "I18n";
+import User from "discourse/models/user";
+import { escapeExpression } from "discourse/lib/utilities";
+import { tracked } from "@glimmer/tracking";
+
+export const THREAD_STATUSES = {
+ open: "open",
+ readOnly: "read_only",
+ closed: "closed",
+ archived: "archived",
+};
+
+export function threadStatusName(status) {
+ switch (status) {
+ case THREAD_STATUSES.open:
+ return I18n.t("chat.thread_status.open");
+ case THREAD_STATUSES.readOnly:
+ return I18n.t("chat.thread_status.read_only");
+ case THREAD_STATUSES.closed:
+ return I18n.t("chat.thread_status.closed");
+ case THREAD_STATUSES.archived:
+ return I18n.t("chat.thread_status.archived");
+ }
+}
+
+const READONLY_STATUSES = [
+ THREAD_STATUSES.closed,
+ THREAD_STATUSES.readOnly,
+ THREAD_STATUSES.archived,
+];
+
+const STAFF_READONLY_STATUSES = [
+ THREAD_STATUSES.readOnly,
+ THREAD_STATUSES.archived,
+];
+
+export default class ChatThread extends RestModel {
+ @tracked title;
+ @tracked status;
+
+ get escapedTitle() {
+ return escapeExpression(this.title);
+ }
+
+ get isOpen() {
+ return !this.status || this.status === THREAD_STATUSES.open;
+ }
+
+ get isReadOnly() {
+ return this.status === THREAD_STATUSES.readOnly;
+ }
+
+ get isClosed() {
+ return this.status === THREAD_STATUSES.closed;
+ }
+
+ get isArchived() {
+ return this.status === THREAD_STATUSES.archived;
+ }
+
+ canModifyMessages(user) {
+ if (user.staff) {
+ return !STAFF_READONLY_STATUSES.includes(this.status);
+ }
+
+ return !READONLY_STATUSES.includes(this.status);
+ }
+}
+
+ChatThread.reopenClass({
+ create(args) {
+ args = args || {};
+ args.original_message_user = User.create(args.original_message_user);
+ return this._super(args);
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-thread.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-thread.js
new file mode 100644
index 0000000000..a37264f16a
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-thread.js
@@ -0,0 +1,31 @@
+import DiscourseRoute from "discourse/routes/discourse";
+import { inject as service } from "@ember/service";
+import { action } from "@ember/object";
+import { schedule } from "@ember/runloop";
+
+export default class ChatChannelThread extends DiscourseRoute {
+ @service chatChannelsManager;
+ @service chat;
+ @service router;
+
+ async model(params) {
+ return this.chatChannelsManager.findThread(params.threadId);
+ }
+
+ afterModel(model) {
+ this.chat.setActiveThread(model);
+ }
+
+ @action
+ didTransition() {
+ const { channelId } = this.paramsFor("chat.channel");
+ const { threadId } = this.paramsFor(this.routeName);
+
+ if (channelId && threadId) {
+ schedule("afterRender", () => {
+ this.chat.openThreadSidebar(channelId, threadId);
+ });
+ }
+ return true;
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
index e0157f0f61..0050a9e55b 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-api.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
@@ -29,6 +29,21 @@ export default class ChatApi extends Service {
);
}
+ /**
+ * Get a thread by its ID.
+ * @param {number} threadId - The ID of the thread.
+ * @returns {Promise}
+ *
+ * @example
+ *
+ * this.chatApi.thread(1).then(thread => { ... })
+ */
+ thread(threadId) {
+ return this.#getRequest(`/threads/${threadId}`).then((result) =>
+ this.chatChannelsManager.storeThread(result.thread)
+ );
+ }
+
/**
* List all accessible category channels of the current user.
* @returns {module:Collection}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
index 9d2cae5539..5970fb49db 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
@@ -1,6 +1,7 @@
import Service, { inject as service } from "@ember/service";
import Promise from "rsvp";
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
+import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
import { tracked } from "@glimmer/tracking";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import { popupAjaxError } from "discourse/lib/ajax-error";
@@ -18,6 +19,7 @@ export default class ChatChannelsManager extends Service {
@service chatApi;
@service currentUser;
@tracked _cached = new TrackedObject();
+ @tracked _cachedThreads = new TrackedObject();
async find(id, options = { fetchIfNotFound: true }) {
const existingChannel = this.#findStale(id);
@@ -30,6 +32,20 @@ export default class ChatChannelsManager extends Service {
}
}
+ // TODO (martin) Maybe we should make a ChatThreadManager as well?
+ // Or have one defined for each channel? We need to keep track of all
+ // threads in channel and have a way to load their messages, and cache.
+ async findThread(id, options = { fetchIfNotFound: true }) {
+ const existingThread = this.#findStaleThread(id);
+ if (existingThread) {
+ return Promise.resolve(existingThread);
+ } else if (options.fetchIfNotFound) {
+ return this.#findThread(id);
+ } else {
+ return Promise.resolve();
+ }
+ }
+
get channels() {
return Object.values(this._cached);
}
@@ -45,6 +61,17 @@ export default class ChatChannelsManager extends Service {
return model;
}
+ storeThread(threadObject) {
+ let model = this.#findStaleThread(threadObject.id);
+
+ if (!model) {
+ model = ChatThread.create(threadObject);
+ this.#cacheThread(model);
+ }
+
+ return model;
+ }
+
async follow(model) {
this.chatSubscriptionsManager.startChannelSubscription(model);
@@ -125,14 +152,32 @@ export default class ChatChannelsManager extends Service {
});
}
+ async #findThread(id) {
+ return this.chatApi
+ .thread(id)
+ .catch(popupAjaxError)
+ .then((thread) => {
+ this.#cacheThread(thread);
+ return thread;
+ });
+ }
+
#cache(channel) {
this._cached[channel.id] = channel;
}
+ #cacheThread(thread) {
+ this._cachedThreads[thread.id] = thread;
+ }
+
#findStale(id) {
return this._cached[id];
}
+ #findStaleThread(id) {
+ return this._cachedThreads[id];
+ }
+
#sortDirectMessageChannels(channels) {
return channels.sort((a, b) => {
const unreadCountA = a.currentUserMembership.unread_count || 0;
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat.js b/plugins/chat/assets/javascripts/discourse/services/chat.js
index 0cdde90a6a..b048d6fd51 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat.js
@@ -36,6 +36,7 @@ export default class Chat extends Service {
@service chatChannelsManager;
activeChannel = null;
+ activeThread = null;
cook = null;
presenceChannel = null;
sidebarActive = false;
@@ -120,6 +121,10 @@ export default class Chat extends Service {
this.set("activeChannel", channel);
}
+ setActiveThread(thread) {
+ this.set("activeThread", thread);
+ }
+
loadCookFunction(categories) {
if (this.cook) {
return Promise.resolve(this.cook);
@@ -271,6 +276,14 @@ export default class Chat extends Service {
});
}
+ async openThreadSidebar(channelId, threadId) {
+ const channel = await this.chatChannelsManager.find(channelId);
+ this.setActiveChannel(channel);
+
+ const thread = await this.chatChannelsManager.findThread(threadId);
+ this.setActiveThread(thread);
+ }
+
async openChannel(channel) {
return this._openFoundChannelAtMessage(channel);
}
diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-thread.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-thread.hbs
new file mode 100644
index 0000000000..89a38629ad
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-thread.hbs
@@ -0,0 +1 @@
+
diff --git a/plugins/chat/assets/stylesheets/common/chat-message-info.scss b/plugins/chat/assets/stylesheets/common/chat-message-info.scss
index fb82db5baa..3438c8ddd2 100644
--- a/plugins/chat/assets/stylesheets/common/chat-message-info.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-message-info.scss
@@ -33,7 +33,8 @@
}
}
-.chat-message-info__date {
+.chat-message-info__date,
+.chat-message-info__thread_id {
color: var(--primary-high);
font-size: var(--font-down-1);
@@ -48,6 +49,9 @@
margin-left: 0.25em;
}
}
+.chat-message-info__thread_id {
+ color: red;
+}
.chat-message-info__flag {
color: var(--secondary-medium);
diff --git a/plugins/chat/assets/stylesheets/common/chat-thread-pane.scss b/plugins/chat/assets/stylesheets/common/chat-thread-pane.scss
new file mode 100644
index 0000000000..e3d5222989
--- /dev/null
+++ b/plugins/chat/assets/stylesheets/common/chat-thread-pane.scss
@@ -0,0 +1,8 @@
+.chat-thread-pane {
+ background-color: #aee6bd;
+ display: none;
+
+ &--active-thread {
+ display: block;
+ }
+}
diff --git a/plugins/chat/assets/stylesheets/common/common.scss b/plugins/chat/assets/stylesheets/common/common.scss
index 340df2cc16..fd27216e88 100644
--- a/plugins/chat/assets/stylesheets/common/common.scss
+++ b/plugins/chat/assets/stylesheets/common/common.scss
@@ -590,6 +590,9 @@ html.has-full-page-chat {
#main-chat-outlet {
min-height: 0;
+
+ display: flex;
+ flex-direction: row;
}
}
}
diff --git a/plugins/chat/lib/chat_message_creator.rb b/plugins/chat/lib/chat_message_creator.rb
index cb2ee31d89..b15bc176f0 100644
--- a/plugins/chat/lib/chat_message_creator.rb
+++ b/plugins/chat/lib/chat_message_creator.rb
@@ -190,5 +190,7 @@ class Chat::ChatMessageCreator
FROM thread_updater
WHERE thread_id IS NULL AND chat_messages.id = thread_updater.id
SQL
+
+ @chat_message.thread_id = thread.id
end
end
diff --git a/plugins/chat/plugin.rb b/plugins/chat/plugin.rb
index 329d07209e..23b7a70ce5 100644
--- a/plugins/chat/plugin.rb
+++ b/plugins/chat/plugin.rb
@@ -68,6 +68,7 @@ register_asset "stylesheets/common/chat-skeleton.scss"
register_asset "stylesheets/colors.scss", :color_definitions
register_asset "stylesheets/common/reviewable-chat-message.scss"
register_asset "stylesheets/common/chat-mention-warnings.scss"
+register_asset "stylesheets/common/chat-thread-pane.scss"
register_asset "stylesheets/common/chat-channel-settings-saved-indicator.scss"
register_svg_icon "comments"
@@ -153,6 +154,7 @@ after_initialize do
load File.expand_path("../app/serializers/chat_channel_serializer.rb", __FILE__)
load File.expand_path("../app/serializers/chat_channel_index_serializer.rb", __FILE__)
load File.expand_path("../app/serializers/chat_channel_search_serializer.rb", __FILE__)
+ load File.expand_path("../app/serializers/chat_thread_serializer.rb", __FILE__)
load File.expand_path("../app/serializers/chat_view_serializer.rb", __FILE__)
load File.expand_path(
"../app/serializers/user_with_custom_fields_and_status_serializer.rb",
@@ -229,6 +231,7 @@ after_initialize do
load File.expand_path("../app/controllers/api/category_chatables_controller.rb", __FILE__)
load File.expand_path("../app/controllers/api/hints_controller.rb", __FILE__)
load File.expand_path("../app/controllers/api/chat_chatables_controller.rb", __FILE__)
+ load File.expand_path("../app/controllers/api/chat_threads_controller.rb", __FILE__)
load File.expand_path("../app/queries/chat_channel_memberships_query.rb", __FILE__)
if Discourse.allow_dev_populate?
@@ -596,6 +599,8 @@ after_initialize do
# Hints for JIT warnings.
get "/mentions/groups" => "hints#check_group_mentions", :format => :json
+
+ get "/threads/:thread_id" => "chat_threads#show"
end
# direct_messages_controller routes
@@ -640,6 +645,7 @@ after_initialize do
base_c_route = "/c/:channel_title/:channel_id"
get base_c_route => "chat#respond", :as => "channel"
get "#{base_c_route}/:message_id" => "chat#respond"
+ get "#{base_c_route}/t/:thread_id" => "chat#respond"
%w[info info/about info/members info/settings].each do |route|
get "#{base_c_route}/#{route}" => "chat#respond"