FEATURE: Add onebox support for chat threads (#23580)

With this commit we now support onboxes of:
- channel
- channel message
- thread
- thread message
This commit is contained in:
Jan Cernik 2023-10-25 09:30:39 -03:00 committed by GitHub
parent a546dcb0cc
commit 3f5a00e20f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 266 additions and 49 deletions

View File

@ -1,6 +1,7 @@
.chat-onebox {
.chat-onebox-body {
.chat-onebox-title {
display: inline;
margin-bottom: 3px;
}
@ -25,6 +26,17 @@
margin-right: 0.25rem;
}
}
.chat-onebox-images.onebox {
display: flex;
flex-wrap: wrap;
img {
width: auto;
max-height: 150px;
margin-right: 0.5rem;
margin-top: 0.5em;
}
}
}
}

View File

@ -24,10 +24,17 @@
padding-bottom: 0.5rem;
}
.chat-transcript-channel {
.chat-transcript-channel,
.chat-transcript-thread {
font-size: var(--font-down-1-rem);
}
.chat-transcript-separator {
font-size: var(--font-down-3-rem);
color: var(--primary-high);
padding: 0 0.5rem;
}
.chat-transcript-username {
color: var(--primary-high-or-secondary-low);
font-weight: bold;
@ -76,6 +83,17 @@
}
}
.chat-transcript-images.onebox {
display: flex;
flex-wrap: wrap;
img {
width: auto;
max-height: 150px;
margin-right: 0.5rem;
margin-top: 0.5em;
}
}
pre code {
box-sizing: border-box;
}

View File

@ -179,6 +179,7 @@ en:
inline_to_message: "Message #%{message_id} by %{username} #%{chat_channel}"
inline_to_channel: "Chat #%{chat_channel}"
inline_to_topic_channel: "Chat for Topic %{topic_title}"
thread_title_connector: "in"
x_members:
one: "%{count} member"

View File

@ -28,4 +28,12 @@ module ::Chat
File.read(path)
end
end
def self.thread_onebox_template
@thread_onebox_template ||=
begin
path = "#{Rails.root}/plugins/chat/lib/onebox/templates/discourse_chat_thread.mustache"
File.read(path)
end
end
end

View File

@ -10,9 +10,13 @@ module Chat
chat_channel = message.chat_channel
user = message.user
return if !chat_channel || !user
thread = Chat::Thread.find_by(id: message.thread_id) if message.thread_id
else
chat_channel = Chat::Channel.find_by(id: route[:channel_id])
return if !chat_channel
thread = Chat::Thread.find_by(id: route[:thread_id]) if route[:thread_id]
end
return if !Guardian.new.can_preview_chat_channel?(chat_channel)
@ -20,9 +24,13 @@ module Chat
args = build_args(url, chat_channel)
if message.present?
render_message_onebox(args, message)
render_message_onebox(args, message, thread)
else
render_channel_onebox(args, chat_channel)
if thread.present?
render_thread_onebox(args, thread)
else
render_channel_onebox(args, chat_channel)
end
end
end
@ -30,7 +38,6 @@ module Chat
def self.build_args(url, chat_channel)
args = {
url: url,
channel_id: chat_channel.id,
channel_name: chat_channel.name,
is_category: chat_channel.category_channel?,
@ -38,7 +45,19 @@ module Chat
}
end
def self.render_message_onebox(args, message)
def self.render_thread_onebox(args, thread)
args.merge!(
cooked: build_thread_snippet(thread),
thread_id: thread.id,
thread_title: thread.title,
thread_title_connector: I18n.t("chat.onebox.thread_title_connector"),
images: get_image_uploads(thread),
)
Mustache.render(Chat.thread_onebox_template, args)
end
def self.render_message_onebox(args, message, thread)
args.merge!(
message_id: message.id,
username: message.user.username,
@ -46,6 +65,9 @@ module Chat
cooked: message.cooked,
created_at: message.created_at,
created_at_str: message.created_at.iso8601,
thread_id: message.thread_id,
thread_title: thread&.title,
images: get_image_uploads(message),
)
Mustache.render(Chat.message_onebox_template, args)
@ -66,6 +88,17 @@ module Chat
Mustache.render(Chat.channel_onebox_template, args)
end
def self.get_image_uploads(target)
if target.is_a?(Message)
message = target
elsif target.is_a?(Thread)
message = Chat::Message.includes(:uploads).find_by(id: target.original_message_id)
end
return if !message
message.uploads.select { |u| u.height.present? || u.width.present? }
end
def self.build_users_list(chat_channel)
Chat::ChannelMembershipsQuery
.call(channel: chat_channel, limit: 10)
@ -82,5 +115,11 @@ module Chat
I18n.t("chat.onebox.and_x_others", count: chat_channel.user_count - users.size)
end
end
def self.build_thread_snippet(thread)
message = Chat::Message.find_by(id: thread.original_message_id)
return nil if !message
message.cooked
end
end
end

View File

@ -1,13 +1,13 @@
<aside class="onebox chat-onebox">
<article class="onebox-body chat-onebox-body">
<h3 class="chat-onebox-title">
<a href="{{url}}">
<a href="/chat/c/-/{{channel_id}}">
{{#is_category}}
<span class="category-chat-badge" style="color: #{{color}}">
<svg class="fa d-icon d-icon-d-chat svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#d-chat"></use></svg>
</span>
{{/is_category}}
<span class="clear-badge">{{{channel_name}}}</span>
<span class="clear-badge">{{channel_name}}</span>
</a>
</h3>
{{#description}}

View File

@ -5,9 +5,9 @@
<img loading="lazy" alt="{{username}}" width="20" height="20" src="{{avatar_url}}" class="avatar">
</a>
</div>
<div class="chat-transcript-username">{{username}}</div>
<div class="chat-transcript-username">{{username}}</div>
<div class="chat-transcript-datetime">
<a href="{{url}}" title="{{created_at}}">{{created_at}}</a>
<a href="/chat/c/-/{{channel_id}}" title="{{created_at}}">{{created_at}}</a>
</div>
<a class="chat-transcript-channel" href="/chat/c/-/{{channel_id}}">
{{#is_category}}
@ -22,6 +22,22 @@
{{/is_topic}}
{{channel_name}}
</a>
{{#thread_id}}
<span class="chat-transcript-separator">&#124</span>
<a class="chat-transcript-thread" href="/chat/c/-/{{channel_id}}/t/{{thread_id}}">
<span class="topic-thread-icon">
<svg class="fa d-icon d-icon-discourse-threads svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#discourse-threads"></use></svg>
</span>
{{thread_title}}
</a>
{{/thread_id}}
</div>
<div class="chat-transcript-messages">
{{{cooked}}}
</div>
<div class="chat-transcript-images onebox">
{{#images}}
<img src="{{url}}" loading="lazy" alt="{{original_filename}}" width="{{width}}" height="{{height}}">
{{/images}}
</div>
<div class="chat-transcript-messages">{{{cooked}}}</div>
</div>

View File

@ -0,0 +1,31 @@
<aside class="onebox chat-onebox">
<article class="onebox-body chat-onebox-body">
<div class="chat-transcript-user">
<h3 class="chat-onebox-title">
<a href="/chat/c/-/{{channel_id}}/t/{{thread_id}}">
<span class="category-chat-badge" style="color: #{{color}}">
<svg class="fa d-icon d-icon-discourse-threads svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#discourse-threads"></use></svg>
</span>
<span class="clear-badge">{{thread_title}}</span>
</a>
</h3>
<span class="thread-title-connector">{{thread_title_connector}}</span>
<a href="/chat/c/-/{{channel_id}}">
<span class="category-chat-badge" style="color: #{{color}}">
<svg class="fa d-icon d-icon-d-chat svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#d-chat"></use></svg>
</span>
<span class="clear-badge">{{channel_name}}</span>
</a>
</div>
{{#cooked}}
<div class="chat-onebox-cooked">
{{{cooked}}}
</div>
{{/cooked}}
<div class="chat-onebox-images onebox">
{{#images}}
<img src="{{url}}" loading="lazy" alt="{{original_filename}}" width="{{width}}" height="{{height}}">
{{/images}}
</div>
</article>
</aside>

View File

@ -27,26 +27,25 @@ describe Chat::OneboxHandler do
onebox_html = Chat::OneboxHandler.handle(public_chat_url, { channel_id: public_channel.id })
expect(onebox_html).to match_html <<~HTML
<aside class="onebox chat-onebox">
<article class="onebox-body chat-onebox-body">
<h3 class="chat-onebox-title">
<a href="#{public_chat_url}">
<span class="category-chat-badge" style="color: ##{public_channel.chatable.color}">
<svg class="fa d-icon d-icon-d-chat svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#d-chat"></use></svg>
</span>
<span class="clear-badge">#{public_channel.name}</span>
</a>
</h3>
<div class="chat-onebox-members-count">1 member</div>
<div class="chat-onebox-members">
<a class="trigger-user-card" data-user-card="#{user.username}" aria-hidden="true" tabindex="-1">
<img loading="lazy" alt="#{user.username}" width="30" height="30" src="#{user.avatar_template_url.gsub("{size}", "60")}" class="avatar">
</a>
</div>
</article>
</aside>
HTML
<aside class="onebox chat-onebox">
<article class="onebox-body chat-onebox-body">
<h3 class="chat-onebox-title">
<a href="/chat/c/-/#{public_channel.id}">
<span class="category-chat-badge" style="color: ##{public_channel.chatable.color}">
<svg class="fa d-icon d-icon-d-chat svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#d-chat"></use></svg>
</span>
<span class="clear-badge">#{public_channel.name}</span>
</a>
</h3>
<div class="chat-onebox-members-count">1 member</div>
<div class="chat-onebox-members">
<a class="trigger-user-card" data-user-card="#{user.username}" aria-hidden="true" tabindex="-1">
<img loading="lazy" alt="#{user.username}" width="30" height="30" src="#{user.avatar_template_url.gsub("{size}", "60")}" class="avatar">
</a>
</div>
</article>
</aside>
HTML
end
end
@ -85,27 +84,28 @@ describe Chat::OneboxHandler do
)
expect(onebox_html).to match_html <<~HTML
<div class="chat-transcript" data-message-id="#{public_message.id}" data-username="#{user.username}" data-datetime="#{public_message.created_at.iso8601}" data-channel-name="#{public_channel.name}" data-channel-id="#{public_channel.id}">
<div class="chat-transcript-user">
<div class="chat-transcript-user-avatar">
<a class="trigger-user-card" data-user-card="#{user.username}" aria-hidden="true" tabindex="-1">
<img loading="lazy" alt="#{user.username}" width="20" height="20" src="#{user.avatar_template_url.gsub("{size}", "20")}" class="avatar">
</a>
</div>
<div class="chat-transcript-username">#{user.username}</div>
<div class="chat-transcript-datetime">
<a href="#{public_chat_url}/#{public_message.id}" title="#{public_message.created_at}">#{public_message.created_at}</a>
<div class="chat-transcript" data-message-id="#{public_message.id}" data-username="#{user.username}" data-datetime="#{public_message.created_at.iso8601}" data-channel-name="#{public_channel.name}" data-channel-id="#{public_channel.id}">
<div class="chat-transcript-user">
<div class="chat-transcript-user-avatar">
<a class="trigger-user-card" data-user-card="#{user.username}" aria-hidden="true" tabindex="-1">
<img loading="lazy" alt="#{user.username}" width="20" height="20" src="#{user.avatar_template_url.gsub("{size}", "20")}" class="avatar">
</a>
</div>
<div class="chat-transcript-username">#{user.username}</div>
<div class="chat-transcript-datetime">
<a href="/chat/c/-/#{public_channel.id}" title="#{public_message.created_at}">#{public_message.created_at}</a>
</div>
<a class="chat-transcript-channel" href="/chat/c/-/#{public_channel.id}">
<span class="category-chat-badge" style="color: ##{public_channel.chatable.color}">
<svg class="fa d-icon d-icon-d-chat svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#d-chat"></use></svg>
</span>
#{public_channel.name}
</a>
</div>
<a class="chat-transcript-channel" href="/chat/c/-/#{public_channel.id}">
<span class="category-chat-badge" style="color: ##{public_channel.chatable.color}">
<svg class="fa d-icon d-icon-d-chat svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#d-chat"></use></svg>
</span>
#{public_channel.name}
</a>
<div class="chat-transcript-messages"><p>Hello world!</p></div>
<div class="chat-transcript-images onebox"></div>
</div>
<div class="chat-transcript-messages"><p>Hello world!</p></div>
</div>
HTML
HTML
end
end
@ -133,4 +133,96 @@ describe Chat::OneboxHandler do
end
end
end
describe "chat thread" do
fab!(:original_public_message) do
Fabricate(:chat_message, user: user, chat_channel: public_channel, message: "Hello world!")
end
fab!(:public_thread) do
Fabricate(:chat_thread, channel: public_channel, original_message: original_public_message)
end
fab!(:private_thread) { Fabricate(:chat_thread, channel: private_channel) }
context "when valid" do
it "renders thread onebox" do
onebox_html =
Chat::OneboxHandler.handle(
"#{public_chat_url}/t/#{public_thread.id}",
{ channel_id: public_channel.id, thread_id: public_thread.id },
)
expect(onebox_html).to match_html <<~HTML
<aside class="onebox chat-onebox">
<article class="onebox-body chat-onebox-body">
<div class="chat-transcript-user">
<h3 class="chat-onebox-title">
<a href="/chat/c/-/#{public_channel.id}/t/#{public_thread.id}">
<span class="category-chat-badge" style="color: ##{public_channel.chatable.color}">
<svg class="fa d-icon d-icon-discourse-threads svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#discourse-threads"></use></svg>
</span>
<span class="clear-badge">#{public_thread.title}</span>
</a>
</h3>
<span class="thread-title-connector">in</span>
<a href="/chat/c/-/#{public_channel.id}">
<span class="category-chat-badge" style="color: ##{public_channel.chatable.color}">
<svg class="fa d-icon d-icon-d-chat svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#d-chat"></use></svg>
</span>
<span class="clear-badge">#{public_channel.name}</span>
</a>
</div>
<div class="chat-onebox-cooked"><p>Hello world!</p></div>
<div class="chat-onebox-images onebox"></div>
</article>
</aside>
HTML
end
end
context "when channel is private" do
it "does not create a onebox" do
onebox_html =
Chat::OneboxHandler.handle(
"#{private_chat_url}/t/#{private_thread.id}",
{ channel_id: private_channel.id, thread_id: public_thread.id },
)
expect(onebox_html).to be_nil
end
end
context "when thread does not exist" do
it "creates a channel onebox" do
public_channel.add(user)
Chat::Channel.ensure_consistency!
onebox_html =
Chat::OneboxHandler.handle(
"#{public_chat_url}/t/999",
{ channel_id: public_channel.id, thread_id: 999 },
)
expect(onebox_html).to match_html <<~HTML
<aside class="onebox chat-onebox">
<article class="onebox-body chat-onebox-body">
<h3 class="chat-onebox-title">
<a href="/chat/c/-/#{public_channel.id}">
<span class="category-chat-badge" style="color: ##{public_channel.chatable.color}">
<svg class="fa d-icon d-icon-d-chat svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#d-chat"></use></svg>
</span>
<span class="clear-badge">#{public_channel.name}</span>
</a>
</h3>
<div class="chat-onebox-members-count">1 member</div>
<div class="chat-onebox-members">
<a class="trigger-user-card" data-user-card="#{user.username}" aria-hidden="true" tabindex="-1">
<img loading="lazy" alt="#{user.username}" width="30" height="30" src="#{user.avatar_template_url.gsub("{size}", "60")}" class="avatar">
</a>
</div>
</article>
</aside>
HTML
end
end
end
end