FEATURE: Add thumbnails for chat image uploads (#24328)
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:
parent
30d5e752d7
commit
8b46dc8bb5
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue