diff --git a/api_docs/changelog.md b/api_docs/changelog.md
index 6eb250d328..dba6d76e08 100644
--- a/api_docs/changelog.md
+++ b/api_docs/changelog.md
@@ -20,6 +20,14 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 9.0
+**Feature level 275**
+
+* [`POST /register`](/api/register-queue), [`PATCH
+ /settings`](/api/update-settings), [`PATCH
+ /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults):
+ Added new `web_animate_image_previews` setting, which controls how
+ animated images should be played in the web/desktop app message feed.
+
**Feature level 274**
* [`GET /events`](/api/get-events): `delete_message` events are now
diff --git a/version.py b/version.py
index de9464f90e..cdc2fce7a0 100644
--- a/version.py
+++ b/version.py
@@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`.
-API_FEATURE_LEVEL = 274 # Last bumped for `delete_message` event.
+API_FEATURE_LEVEL = 275 # Last bumped for `web_animate_image_previews` setting.
# Bump the minor PROVISION_VERSION to indicate that folks should provision
diff --git a/web/src/admin.js b/web/src/admin.js
index dc875f2ec1..2bbee868bb 100644
--- a/web/src/admin.js
+++ b/web/src/admin.js
@@ -194,6 +194,7 @@ export function build_page() {
user_list_style_values: settings_config.user_list_style_values,
web_stream_unreads_count_display_policy_values:
settings_config.web_stream_unreads_count_display_policy_values,
+ web_animate_image_previews_values: settings_config.web_animate_image_previews_values,
color_scheme_values: settings_config.color_scheme_values,
web_home_view_values: settings_config.web_home_view_values,
settings_object: realm_user_settings_defaults,
diff --git a/web/src/click_handlers.js b/web/src/click_handlers.js
index 61ce74c12f..2a1fa151e9 100644
--- a/web/src/click_handlers.js
+++ b/web/src/click_handlers.js
@@ -119,6 +119,7 @@ export function initialize() {
// Inline image, video and twitter previews.
if (
$target.is("img.message_inline_image") ||
+ $target.is(".message_inline_animated_image_still") ||
$target.is("video") ||
$target.is(".message_inline_video") ||
$target.is("img.twitter-avatar")
diff --git a/web/src/lightbox.ts b/web/src/lightbox.ts
index 35eb9699ca..b6ff0e3b45 100644
--- a/web/src/lightbox.ts
+++ b/web/src/lightbox.ts
@@ -580,7 +580,7 @@ export function initialize(): void {
$("#main_div, #compose .preview_content").on(
"click",
- ".message_inline_image:not(.message_inline_video) a",
+ ".message_inline_image:not(.message_inline_video) a, .message_inline_animated_image_still",
function (e) {
// prevent the link from opening in a new page.
e.preventDefault();
diff --git a/web/src/message_list_hover.js b/web/src/message_list_hover.js
index 9400a7ea3a..a59926f764 100644
--- a/web/src/message_list_hover.js
+++ b/web/src/message_list_hover.js
@@ -6,6 +6,8 @@ import render_edit_content_button from "../templates/edit_content_button.hbs";
import * as message_edit from "./message_edit";
import * as message_lists from "./message_lists";
import * as rows from "./rows";
+import * as thumbnail from "./thumbnail";
+import {user_settings} from "./user_settings";
let $current_message_hover;
export function message_unhover() {
@@ -73,6 +75,40 @@ export function initialize() {
$row.removeClass("sender_info_hovered");
});
+ $("#main_div").on(
+ "mouseover",
+ '.message-list div.message_inline_image img[data-animated="true"]',
+ function () {
+ if (user_settings.web_animate_image_previews !== "on_hover") {
+ return;
+ }
+ const $img = $(this);
+ $img.closest(".message_inline_image").removeClass(
+ "message_inline_animated_image_still",
+ );
+ $img.attr(
+ "src",
+ $img.attr("src").replace(/\/[^/]+$/, "/" + thumbnail.animated_format.name),
+ );
+ },
+ );
+
+ $("#main_div").on(
+ "mouseout",
+ '.message-list div.message_inline_image img[data-animated="true"]',
+ function () {
+ if (user_settings.web_animate_image_previews !== "on_hover") {
+ return;
+ }
+ const $img = $(this);
+ $img.closest(".message_inline_image").addClass("message_inline_animated_image_still");
+ $img.attr(
+ "src",
+ $img.attr("src").replace(/\/[^/]+$/, "/" + thumbnail.preferred_format.name),
+ );
+ },
+ );
+
function handle_video_preview_mouseenter($elem) {
// Set image height and css vars for play button position, if not done already
const setPosition = !$elem.data("entered-before");
diff --git a/web/src/realm_user_settings_defaults.ts b/web/src/realm_user_settings_defaults.ts
index e67562501b..ffbd4eb313 100644
--- a/web/src/realm_user_settings_defaults.ts
+++ b/web/src/realm_user_settings_defaults.ts
@@ -52,6 +52,7 @@ export const realm_default_settings_schema = z.object({
translate_emoticons: z.boolean(),
twenty_four_hour_time: z.boolean(),
user_list_style: z.number(),
+ web_animate_image_previews: z.string(),
web_channel_default_view: z.number(),
web_escape_navigates_to_home_view: z.boolean(),
web_font_size_px: z.number(),
diff --git a/web/src/rendered_markdown.ts b/web/src/rendered_markdown.ts
index a96e0c1e8a..f9fc2cd75d 100644
--- a/web/src/rendered_markdown.ts
+++ b/web/src/rendered_markdown.ts
@@ -345,7 +345,21 @@ export const update_elements = ($content: JQuery): void => {
const $inline_img_thumbnail = $(this);
let thumbnail_name = thumbnail.preferred_format.name;
if ($inline_img_thumbnail.attr("data-animated") === "true") {
- thumbnail_name = thumbnail.animated_format.name;
+ if (
+ user_settings.web_animate_image_previews === "always" ||
+ // Treat on_hover as "always" on mobile web, where
+ // hovering is impossible and there's much less on
+ // the screen.
+ (user_settings.web_animate_image_previews === "on_hover" && util.is_mobile())
+ ) {
+ thumbnail_name = thumbnail.animated_format.name;
+ } else {
+ // If we're showing a still thumbnail, show a play
+ // button so that users that it can be played.
+ $inline_img_thumbnail
+ .closest(".message_inline_image")
+ .addClass("message_inline_animated_image_still");
+ }
}
$inline_img_thumbnail.attr(
"src",
diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js
index baccea2ac8..3df1434fe3 100644
--- a/web/src/server_events_dispatch.js
+++ b/web/src/server_events_dispatch.js
@@ -728,6 +728,7 @@ export function dispatch_normal_event(event) {
"translate_emoticons",
"display_emoji_reaction_users",
"user_list_style",
+ "web_animate_image_previews",
"web_stream_unreads_count_display_policy",
"starred_message_counts",
"send_stream_typing_notifications",
@@ -779,6 +780,12 @@ export function dispatch_normal_event(event) {
stream_list.update_streams_sidebar();
stream_list_sort.set_filter_out_inactives();
}
+ if (event.property === "web_animate_image_previews") {
+ // Rerender the whole message list UI
+ for (const msg_list of message_lists.all_rendered_message_lists()) {
+ msg_list.rerender();
+ }
+ }
if (event.property === "web_stream_unreads_count_display_policy") {
stream_list.update_dom_unread_counts_visibility();
}
diff --git a/web/src/settings.js b/web/src/settings.js
index 187d3093db..0c7d6b4fb9 100644
--- a/web/src/settings.js
+++ b/web/src/settings.js
@@ -105,6 +105,7 @@ export function build_page() {
settings_config.web_mark_read_on_scroll_policy_values,
web_channel_default_view_values: settings_config.web_channel_default_view_values,
user_list_style_values: settings_config.user_list_style_values,
+ web_animate_image_previews_values: settings_config.web_animate_image_previews_values,
web_stream_unreads_count_display_policy_values:
settings_config.web_stream_unreads_count_display_policy_values,
color_scheme_values: settings_config.color_scheme_values,
diff --git a/web/src/settings_config.ts b/web/src/settings_config.ts
index 22ed27b57f..dfa589618d 100644
--- a/web/src/settings_config.ts
+++ b/web/src/settings_config.ts
@@ -84,6 +84,21 @@ export const user_list_style_values = {
// },
};
+export const web_animate_image_previews_values = {
+ always: {
+ code: "always",
+ description: $t({defaultMessage: "Always"}),
+ },
+ on_hover: {
+ code: "on_hover",
+ description: $t({defaultMessage: "On hover"}),
+ },
+ never: {
+ code: "never",
+ description: $t({defaultMessage: "Only in image viewer"}),
+ },
+};
+
export const web_stream_unreads_count_display_policy_values = {
all_streams: {
code: 1,
diff --git a/web/src/settings_preferences.ts b/web/src/settings_preferences.ts
index 38d0954b92..9f0d90de9b 100644
--- a/web/src/settings_preferences.ts
+++ b/web/src/settings_preferences.ts
@@ -230,6 +230,9 @@ export function set_up(settings_panel: SettingsPanel): void {
.find(`.setting_user_list_style_choice[value=${settings_object.user_list_style}]`)
.prop("checked", true);
+ $container
+ .find(".setting_web_animate_image_previews")
+ .val(settings_object.web_animate_image_previews);
$container
.find(".setting_web_stream_unreads_count_display_policy")
.val(settings_object.web_stream_unreads_count_display_policy);
diff --git a/web/src/user_settings.ts b/web/src/user_settings.ts
index 4e5a8b755e..446856afc1 100644
--- a/web/src/user_settings.ts
+++ b/web/src/user_settings.ts
@@ -71,6 +71,7 @@ export const user_settings_schema = stream_notification_settings_schema
translate_emoticons: z.boolean(),
twenty_four_hour_time: z.boolean(),
user_list_style: z.number(),
+ web_animate_image_previews: z.enum(["always", "on_hover", "never"]),
web_channel_default_view: z.number(),
web_escape_navigates_to_home_view: z.boolean(),
web_font_size_px: z.number(),
diff --git a/web/styles/rendered_markdown.css b/web/styles/rendered_markdown.css
index 086e3fc37f..26966fbaa0 100644
--- a/web/styles/rendered_markdown.css
+++ b/web/styles/rendered_markdown.css
@@ -564,7 +564,8 @@
float: none;
}
- .message_inline_video {
+ .message_inline_video,
+ .message_inline_animated_image_still {
&:hover {
&::after {
transform: scale(1);
diff --git a/web/templates/settings/display_settings_information.hbs b/web/templates/settings/display_settings_information.hbs
index f68d91f973..96bbbb1885 100644
--- a/web/templates/settings/display_settings_information.hbs
+++ b/web/templates/settings/display_settings_information.hbs
@@ -35,6 +35,13 @@
+
+
+
+
+