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 ||
null;
const _fullsizeURL = item.href || item.src || innerImage.src || null;
const _fullsizeURL =
item.dataset?.largeSrc ||
item.href ||
item.src ||
innerImage.src ||
null;
const _smallURL =
innerImage.currentSrc ||

@ -222,15 +222,6 @@ class CookedPostProcessor
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)
w, h = img["width"].to_i, img["height"].to_i
onebox = img.ancestors(".onebox, .onebox-body").first

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

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

@ -161,7 +161,7 @@ export default {
},
callbacks: {
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);
if (message) {
message.cooked = data.chat_message.cooked;
message.uploads = cloneJSON(data.chat_message.uploads || []);
message.processed = true;
message.incrementVersion();
}

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

@ -17,9 +17,39 @@ module Chat
def run!
post_process_oneboxes
process_thumbnails
DiscourseEvent.trigger(:chat_message_processed, @doc, @model)
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
[]
end

@ -253,6 +253,12 @@ after_initialize do
object.chat_separate_sidebar_mode
end
add_to_serializer(
:upload,
:thumbnail,
include_condition: -> { SiteSetting.chat_enabled && SiteSetting.create_thumbnails },
) { object.thumbnail }
RETENTION_SETTINGS_TO_USER_OPTION_FIELDS = {
chat_channel_retention_days: :dismissed_channel_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
before do
Jobs.run_immediately!
channel_1.add(current_user)
sign_in(current_user)
end
@ -39,6 +40,34 @@ describe "Uploading files in chat messages", type: :system do
expect(Chat::Message.last.uploads.count).to eq(1)
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
chat.visit_channel(channel_1)
file_path = file_from_fixtures("logo.png", "images").path