FEATURE: Add thumbnails for chat image uploads ()

Introduces the concept of image thumbnails in chat, prior to this we uploaded and used full size chat images within channels and direct messages.

The following changes are covered:
- Post processing of image uploads to create the thumbnail within Chat::MessageProcessor
- Extract responsive image ratios into CookedProcessorMixin (used for creating upload variations)
- Add thumbnail to upload serializer from plugin.rb
- Convert chat upload template to glimmer component using .gjs format
- Use thumbnail image within chat upload component (stores full size img in orig-src data attribute)
- Old uploads which don't have thumbnails will fallback to full size images in channels/DMs
- Update Magnific lightbox to use full size image when clicked
- Update Glimmer lightbox to use full size image (enables zooming for chat images)
This commit is contained in:
David Battersby 2023-12-06 14:59:18 +08:00 committed by GitHub
parent 30d5e752d7
commit 8b46dc8bb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 90 additions and 13 deletions

@ -23,7 +23,12 @@ export async function processHTML({ container, selector, clickTarget }) {
item.parentElement?.style?.backgroundImage || item.parentElement?.style?.backgroundImage ||
null; null;
const _fullsizeURL = item.href || item.src || innerImage.src || null; const _fullsizeURL =
item.dataset?.largeSrc ||
item.href ||
item.src ||
innerImage.src ||
null;
const _smallURL = const _smallURL =
innerImage.currentSrc || innerImage.currentSrc ||

@ -222,15 +222,6 @@ class CookedPostProcessor
end end
end end
def each_responsive_ratio
SiteSetting
.responsive_post_image_sizes
.split("|")
.map(&:to_f)
.sort
.each { |r| yield r if r > 1 }
end
def optimize_image!(img, upload, cropped: false) def optimize_image!(img, upload, cropped: false)
w, h = img["width"].to_i, img["height"].to_i w, h = img["width"].to_i, img["height"].to_i
onebox = img.ancestors(".onebox, .onebox-body").first onebox = img.ancestors(".onebox, .onebox-body").first

@ -362,4 +362,13 @@ module CookedProcessorMixin
span.content = content if content span.content = content if content
span span
end end
def each_responsive_ratio
SiteSetting
.responsive_post_image_sizes
.split("|")
.map(&:to_f)
.sort
.each { |r| yield r if r > 1 }
end
end end

@ -7,7 +7,7 @@ import { htmlSafe } from "@ember/template";
import { isAudio, isImage, isVideo } from "discourse/lib/uploads"; import { isAudio, isImage, isVideo } from "discourse/lib/uploads";
import eq from "truth-helpers/helpers/eq"; import eq from "truth-helpers/helpers/eq";
export default class extends Component { export default class ChatUpload extends Component {
@service siteSettings; @service siteSettings;
@tracked loaded = false; @tracked loaded = false;
@ -44,6 +44,10 @@ export default class extends Component {
return { width: width * ratio, height: height * ratio }; return { width: width * ratio, height: height * ratio };
} }
get imageUrl() {
return this.args.upload.thumbnail?.url ?? this.args.upload.url;
}
get imageStyle() { get imageStyle() {
if (this.args.upload.dominant_color && !this.loaded) { if (this.args.upload.dominant_color && !this.loaded) {
return htmlSafe(`background-color: #${this.args.upload.dominant_color};`); return htmlSafe(`background-color: #${this.args.upload.dominant_color};`);
@ -60,9 +64,10 @@ export default class extends Component {
<img <img
class="chat-img-upload" class="chat-img-upload"
data-orig-src={{@upload.short_url}} data-orig-src={{@upload.short_url}}
data-large-src={{@upload.url}}
height={{this.size.height}} height={{this.size.height}}
width={{this.size.width}} width={{this.size.width}}
src={{@upload.url}} src={{this.imageUrl}}
style={{this.imageStyle}} style={{this.imageStyle}}
loading="lazy" loading="lazy"
tabindex="0" tabindex="0"

@ -161,7 +161,7 @@ export default {
}, },
callbacks: { callbacks: {
elementParse: (item) => { elementParse: (item) => {
item.src = item.el[0].src; item.src = item.el[0].dataset.largeSrc || item.el[0].src;
}, },
}, },
}); });

@ -126,6 +126,7 @@ export default class ChatChannelSubscriptionManager {
const message = this.messagesManager.findMessage(data.chat_message.id); const message = this.messagesManager.findMessage(data.chat_message.id);
if (message) { if (message) {
message.cooked = data.chat_message.cooked; message.cooked = data.chat_message.cooked;
message.uploads = cloneJSON(data.chat_message.uploads || []);
message.processed = true; message.processed = true;
message.incrementVersion(); message.incrementVersion();
} }

@ -116,6 +116,7 @@ export default class ChatChannelThreadSubscriptionManager {
const message = this.messagesManager.findMessage(data.chat_message.id); const message = this.messagesManager.findMessage(data.chat_message.id);
if (message) { if (message) {
message.cooked = data.chat_message.cooked; message.cooked = data.chat_message.cooked;
message.uploads = cloneJSON(data.chat_message.uploads || []);
message.processed = true; message.processed = true;
message.incrementVersion(); message.incrementVersion();
} }

@ -17,9 +17,39 @@ module Chat
def run! def run!
post_process_oneboxes post_process_oneboxes
process_thumbnails
DiscourseEvent.trigger(:chat_message_processed, @doc, @model) DiscourseEvent.trigger(:chat_message_processed, @doc, @model)
end end
def process_thumbnails
@model.uploads.each do |upload|
if upload.width <= SiteSetting.max_image_width &&
upload.height <= SiteSetting.max_image_height
return false
end
crop =
SiteSetting.min_ratio_to_crop > 0 &&
upload.width.to_f / upload.height.to_f < SiteSetting.min_ratio_to_crop
width = upload.thumbnail_width
height = upload.thumbnail_height
# create the main thumbnail
upload.create_thumbnail!(width, height, crop: crop)
# create additional responsive thumbnails
each_responsive_ratio do |ratio|
resized_w = (width * ratio).to_i
resized_h = (height * ratio).to_i
if upload.width && resized_w <= upload.width
upload.create_thumbnail!(resized_w, resized_h, crop: crop)
end
end
end
end
def large_images def large_images
[] []
end end

@ -253,6 +253,12 @@ after_initialize do
object.chat_separate_sidebar_mode object.chat_separate_sidebar_mode
end end
add_to_serializer(
:upload,
:thumbnail,
include_condition: -> { SiteSetting.chat_enabled && SiteSetting.create_thumbnails },
) { object.thumbnail }
RETENTION_SETTINGS_TO_USER_OPTION_FIELDS = { RETENTION_SETTINGS_TO_USER_OPTION_FIELDS = {
chat_channel_retention_days: :dismissed_channel_retention_reminder, chat_channel_retention_days: :dismissed_channel_retention_reminder,
chat_dm_retention_days: :dismissed_dm_retention_reminder, chat_dm_retention_days: :dismissed_dm_retention_reminder,

@ -12,6 +12,7 @@ describe "Uploading files in chat messages", type: :system do
context "when uploading to a new message" do context "when uploading to a new message" do
before do before do
Jobs.run_immediately!
channel_1.add(current_user) channel_1.add(current_user)
sign_in(current_user) sign_in(current_user)
end end
@ -39,6 +40,34 @@ describe "Uploading files in chat messages", type: :system do
expect(Chat::Message.last.uploads.count).to eq(1) expect(Chat::Message.last.uploads.count).to eq(1)
end end
it "adds a thumbnail for large images" do
SiteSetting.create_thumbnails = true
chat.visit_channel(channel_1)
file_path = file_from_fixtures("huge.jpg", "images").path
attach_file(file_path) do
channel_page.open_action_menu
channel_page.click_action_button("chat-upload-btn")
end
expect { channel_page.send_message }.to change { Chat::Message.count }.by(1)
expect(channel_page).to have_no_css(".chat-composer-upload")
message = Chat::Message.last
try_until_success(timeout: 5) { expect(message.uploads.first.thumbnail).to be_present }
upload = message.uploads.first
# image has src attribute with thumbnail url
expect(channel_page).to have_css(".chat-uploads img[src$='#{upload.thumbnail.url}']")
# image has data-large-src with original image src
expect(channel_page).to have_css(".chat-uploads img[data-large-src$='#{upload.url}']")
end
it "adds dominant color attribute to images" do it "adds dominant color attribute to images" do
chat.visit_channel(channel_1) chat.visit_channel(channel_1)
file_path = file_from_fixtures("logo.png", "images").path file_path = file_from_fixtures("logo.png", "images").path