settings: Add setting to control how animated images are played.

Previously animated images were automatically played in the
message feed of the web app.

Now that we have still thumbnails available for them, we can add a new
personal setting, "web_animate_image_previews", which controls how the
animated images would be played in the web app message feed -- always
played, on hover, or only in the image viewer.

Fixes #31016.
This commit is contained in:
roanster007 2024-07-21 18:53:56 +05:30 committed by Tim Abbott
parent 1c30ea1819
commit 66a96bee71
27 changed files with 251 additions and 5 deletions

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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")

View File

@ -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();

View File

@ -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");

View File

@ -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(),

View File

@ -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",

View File

@ -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();
}

View File

@ -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,

View File

@ -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,

View File

@ -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);

View File

@ -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(),

View File

@ -564,7 +564,8 @@
float: none;
}
.message_inline_video {
.message_inline_video,
.message_inline_animated_image_still {
&:hover {
&::after {
transform: scale(1);

View File

@ -35,6 +35,13 @@
</div>
</div>
<div class="input-group thinner setting-next-is-related">
<label for="web_animate_image_previews" class="settings-field-label">{{t "Play animated images" }}</label>
<select name="web_animate_image_previews" class="setting_web_animate_image_previews prop-element settings_select bootstrap-focus-style" id="{{prefix}}web_animate_image_previews" data-setting-widget-type="string">
{{> dropdown_options_widget option_values=web_animate_image_previews_values}}
</select>
</div>
<div class="input-group">
<label for="web_stream_unreads_count_display_policy" class="settings-field-label">{{t "Show unread counts for" }}</label>
<select name="web_stream_unreads_count_display_policy" class="setting_web_stream_unreads_count_display_policy prop-element bootstrap-focus-style settings_select" id="{{prefix}}web_stream_unreads_count_display_policy" data-setting-widget-type="number">

View File

@ -1031,6 +1031,26 @@ run_test("user_settings", ({override}) => {
dispatch(event);
assert.equal(user_settings.web_home_view, "inbox");
}
{
event = event_fixtures.user_settings__web_animate_image_previews_always;
user_settings.web_animate_image_previews = "on_hover";
dispatch(event);
assert.equal(user_settings.web_animate_image_previews, "always");
}
{
event = event_fixtures.user_settings__web_animate_image_previews_on_hover;
user_settings.web_animate_image_previews = "never";
dispatch(event);
assert.equal(user_settings.web_animate_image_previews, "on_hover");
}
{
event = event_fixtures.user_settings__web_animate_image_previews_never;
user_settings.web_animate_image_previews = "always";
dispatch(event);
assert.equal(user_settings.web_animate_image_previews, "never");
}
{
const stub = make_stub();

View File

@ -1023,6 +1023,27 @@ exports.fixtures = {
value: 2,
},
user_settings__web_animate_image_previews_always: {
type: "user_settings",
op: "update",
property: "web_animate_image_previews",
value: "always",
},
user_settings__web_animate_image_previews_never: {
type: "user_settings",
op: "update",
property: "web_animate_image_previews",
value: "never",
},
user_settings__web_animate_image_previews_on_hover: {
type: "user_settings",
op: "update",
property: "web_animate_image_previews",
value: "on_hover",
},
user_settings__web_channel_default_view: {
type: "user_settings",
op: "update",

View File

@ -201,7 +201,7 @@ run_test("message_inline_video", () => {
window.GestureEvent = false;
});
run_test("message_inline_animated_image", ({override}) => {
run_test("message_inline_animated_image_still", ({override}) => {
const $content = get_content_element();
const $elem = $.create("img");
@ -213,6 +213,13 @@ run_test("message_inline_animated_image", ({override}) => {
$array([$elem]),
);
const $stub = $.create("div.message_inline_image");
$elem.closest = (closest_opts) => {
assert.equal(closest_opts, ".message_inline_image");
return $stub;
};
const thumbnail_formats = [
{
name: "840x560-anim.webp",
@ -257,9 +264,22 @@ run_test("message_inline_animated_image", ({override}) => {
rm.update_elements($content);
assert.equal($elem.attr("src"), "/path/to/840x560.webp");
// Now verify the behavior for animated images.
$elem.attr("data-animated", "true");
override(user_settings, "web_animate_image_previews", "always");
rm.update_elements($content);
assert.equal($elem.attr("src"), "/path/to/840x560-anim.webp");
// And verify the different behavior for other values of the animation setting.
override(user_settings, "web_animate_image_previews", "on_hover");
rm.update_elements($content);
assert.equal($elem.attr("src"), "/path/to/840x560.webp");
assert.equal($stub.hasClass("message_inline_animated_image_still"), true);
override(user_settings, "web_animate_image_previews", "never");
rm.update_elements($content);
assert.equal($elem.attr("src"), "/path/to/840x560.webp");
assert.equal($stub.hasClass("message_inline_animated_image_still"), true);
});
run_test("user-mention", () => {

View File

@ -0,0 +1,22 @@
# Generated by Django 5.0.6 on 2024-07-20 10:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0557_change_information_density_defaults"),
]
operations = [
migrations.AddField(
model_name="realmuserdefault",
name="web_animate_image_previews",
field=models.TextField(default="on_hover"),
),
migrations.AddField(
model_name="userprofile",
name="web_animate_image_previews",
field=models.TextField(default="on_hover"),
),
]

View File

@ -89,6 +89,9 @@ class UserBaseSettings(models.Model):
default=WEB_LINE_HEIGHT_PERCENT_DEFAULT
)
# UI setting to control how animated images are played.
web_animate_image_previews = models.TextField(default="on_hover")
# UI setting controlling Zulip's behavior of demoting in the sort
# order and graying out streams with no recent traffic. The
# default behavior, automatic, enables this behavior once a user
@ -350,6 +353,7 @@ class UserBaseSettings(models.Model):
web_mark_read_on_scroll_policy=int,
web_channel_default_view=int,
user_list_style=int,
web_animate_image_previews=str,
web_stream_unreads_count_display_policy=int,
web_font_size_px=int,
web_line_height_percent=int,

View File

@ -11334,6 +11334,23 @@ paths:
- 2
- 3
example: 1
web_animate_image_previews:
description: |
Controls how animated images should be played in the message feed in the web/desktop application.
- "always" - Always play the animated images in the message feed.
- "on_hover" - Play the animated images on hover over them in the message feed.
- "never" - Never play animated images in the message feed.
**Changes**: New in Zulip 9.0 (feature level 275). Previously, animated images
always used to play in the message feed by default. This setting controls this
behaviour.
type: string
enum:
- always
- on_hover
- never
example: on_hover
web_stream_unreads_count_display_policy:
description: |
Configuration for which channels should be displayed with a numeric unread count in the left sidebar.
@ -14304,6 +14321,16 @@ paths:
- 3 - With avatar and status
**Changes**: New in Zulip 6.0 (feature level 141).
web_animate_image_previews:
type: string
description: |
Controls how animated images should be played in the message feed in the web/desktop application.
- "always" - Always play the animated images in the message feed.
- "on_hover" - Play the animated images on hover over them in the message feed.
- "never" - Never play animated images in the message feed.
**Changes**: New in Zulip 9.0 (feature level 275).
web_stream_unreads_count_display_policy:
type: integer
description: |
@ -16766,6 +16793,16 @@ paths:
- 3 - With avatar and status
**Changes**: New in Zulip 6.0 (feature level 141).
web_animate_image_previews:
type: string
description: |
Controls how animated images should be played in the message feed in the web/desktop application.
- "always" - Always play the animated images in the message feed.
- "on_hover" - Play the animated images on hover over them in the message feed.
- "never" - Never play animated images in the message feed.
**Changes**: New in Zulip 9.0 (feature level 275).
web_stream_unreads_count_display_policy:
type: integer
description: |
@ -17967,6 +18004,21 @@ paths:
- 2
- 3
example: 1
web_animate_image_previews:
description: |
Controls how animated images should be played in the message feed in the web/desktop application.
- "always" - Always play the animated images in the message feed.
- "on_hover" - Play the animated images on hover over them in the message feed.
- "never" - Never play animated images in the message feed.
**Changes**: New in Zulip 9.0 (feature level 275).
type: string
enum:
- always
- on_hover
- never
example: on_hover
web_stream_unreads_count_display_policy:
description: |
Configuration for which channels should be displayed with a numeric unread count in the left sidebar.

View File

@ -775,6 +775,7 @@ class TestRealmAuditLog(ZulipTestCase):
value: bool | int | str
test_values = dict(
default_language="de",
web_animate_image_previews="on_hover",
web_home_view="all_messages",
emojiset="twitter",
notification_sound="ding",

View File

@ -3787,6 +3787,7 @@ class RealmPropertyActionTest(BaseAction):
web_mark_read_on_scroll_policy=UserProfile.WEB_MARK_READ_ON_SCROLL_POLICY_CHOICES,
web_channel_default_view=UserProfile.WEB_CHANNEL_DEFAULT_VIEW_CHOICES,
user_list_style=UserProfile.USER_LIST_STYLE_CHOICES,
web_animate_image_previews=["always", "on_hover", "never"],
web_stream_unreads_count_display_policy=UserProfile.WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES,
desktop_icon_count_display=UserProfile.DESKTOP_ICON_COUNT_DISPLAY_CHOICES,
notification_sound=["zulip", "ding"],
@ -3916,6 +3917,7 @@ class UserDisplayActionTest(BaseAction):
web_mark_read_on_scroll_policy=[2, 3, 1],
web_channel_default_view=[2, 1],
user_list_style=[1, 2, 3],
web_animate_image_previews=["always", "on_hover", "never"],
web_stream_unreads_count_display_policy=[1, 2, 3],
web_font_size_px=[12, 16, 18],
web_line_height_percent=[105, 120, 160],

View File

@ -1946,6 +1946,7 @@ class RealmAPITest(ZulipTestCase):
web_mark_read_on_scroll_policy=UserProfile.WEB_MARK_READ_ON_SCROLL_POLICY_CHOICES,
web_channel_default_view=UserProfile.WEB_CHANNEL_DEFAULT_VIEW_CHOICES,
user_list_style=UserProfile.USER_LIST_STYLE_CHOICES,
web_animate_image_previews=["always", "on_hover", "never"],
web_stream_unreads_count_display_policy=UserProfile.WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES,
desktop_icon_count_display=UserProfile.DESKTOP_ICON_COUNT_DISPLAY_CHOICES,
notification_sound=["zulip", "ding"],

View File

@ -360,6 +360,7 @@ class ChangeSettingsTest(ZulipTestCase):
web_mark_read_on_scroll_policy=2,
web_channel_default_view=2,
user_list_style=2,
web_animate_image_previews="on_hover",
web_stream_unreads_count_display_policy=2,
web_font_size_px=14,
web_line_height_percent=122,
@ -415,6 +416,7 @@ class ChangeSettingsTest(ZulipTestCase):
web_mark_read_on_scroll_policy=10,
web_channel_default_view=10,
user_list_style=10,
web_animate_image_previews="invalid_value",
web_stream_unreads_count_display_policy=10,
color_scheme=10,
notification_sound="invalid_sound",

View File

@ -638,6 +638,7 @@ def update_realm_user_settings_defaults(
Annotated[int, check_int_in_validator(UserProfile.USER_LIST_STYLE_CHOICES)]
]
| None = None,
web_animate_image_previews: Literal["always", "on_hover", "never"] | None = None,
email_address_visibility: Json[
Annotated[int, check_int_in_validator(UserProfile.EMAIL_ADDRESS_VISIBILITY_TYPES)]
]

View File

@ -161,6 +161,7 @@ def confirm_email_change(request: HttpRequest, confirmation_key: str) -> HttpRes
emojiset_choices = {emojiset["key"] for emojiset in UserProfile.emojiset_choices()}
web_home_view_options = ["recent_topics", "inbox", "all_messages"]
web_animate_image_previews_options = ["always", "on_hover", "never"]
def check_settings_values(
@ -329,6 +330,9 @@ def json_change_settings(
user_list_style: int | None = REQ(
json_validator=check_int_in(UserProfile.USER_LIST_STYLE_CHOICES), default=None
),
web_animate_image_previews: str | None = REQ(
str_validator=check_string_in(web_animate_image_previews_options), default=None
),
email_address_visibility: int | None = REQ(
json_validator=check_int_in(UserProfile.EMAIL_ADDRESS_VISIBILITY_TYPES), default=None
),