diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 5606c48f67..07313941e5 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,11 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 9.0 +**Feature level 273** + +* [`POST /register`](/api/register-queue): Added `server_thumbnail_formats` + describing what formats the server will thumbnail images into. + **Feature level 272** * [`POST /user_uploads`](/api/upload-file): `uri` was renamed diff --git a/tools/test-js-with-node b/tools/test-js-with-node index c3a24bc56c..cea9e57f1e 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -252,6 +252,7 @@ EXEMPT_FILES = make_set( "web/src/submessage.ts", "web/src/subscriber_api.ts", "web/src/theme.ts", + "web/src/thumbnail.ts", "web/src/timerender.ts", "web/src/tippyjs.ts", "web/src/todo_widget.js", diff --git a/version.py b/version.py index 34849354b4..eff62ac052 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 = 272 # Last bumped for "POST /user_uploads" +API_FEATURE_LEVEL = 273 # Last bumped for server_thumbnail_formats # Bump the minor PROVISION_VERSION to indicate that folks should provision diff --git a/web/src/state_data.ts b/web/src/state_data.ts index 89b6e27aac..017a8febab 100644 --- a/web/src/state_data.ts +++ b/web/src/state_data.ts @@ -179,6 +179,14 @@ const one_time_notice_schema = z.object({ type: z.literal("one_time_notice"), }); +export const thumbnail_format_schema = z.object({ + name: z.string(), + max_width: z.number(), + max_height: z.number(), + format: z.string(), + animated: z.boolean(), +}); + /* We may introduce onboarding step of types other than 'one time notice' in future. Earlier, we had 'hotspot' and 'one time notice' as the two types. We can simply do: @@ -374,6 +382,7 @@ const realm_schema = z.object({ stream: z.record(group_permission_setting_schema), group: z.record(group_permission_setting_schema), }), + server_thumbnail_formats: z.array(thumbnail_format_schema), server_typing_started_expiry_period_milliseconds: z.number(), server_typing_started_wait_period_milliseconds: z.number(), server_typing_stopped_wait_period_milliseconds: z.number(), diff --git a/web/src/thumbnail.ts b/web/src/thumbnail.ts new file mode 100644 index 0000000000..b5c30abce6 --- /dev/null +++ b/web/src/thumbnail.ts @@ -0,0 +1,36 @@ +import type {z} from "zod"; + +import {realm} from "./state_data"; +import type {thumbnail_format_schema} from "./state_data"; + +type ThumbnailFormat = z.infer; + +export const thumbnail_formats: ThumbnailFormat[] = []; + +export let preferred_format: ThumbnailFormat; +export let animated_format: ThumbnailFormat; + +export function initialize(): void { + // Go looking for the size closest to 300x200, of the smallest format. We assume all browsers + // support webp. + const format_preferences = ["webp", "jpg", "gif"]; + const sorted_formats = realm.server_thumbnail_formats.sort((a, b) => { + if (a.max_width !== b.max_width) { + return Math.abs(a.max_width - 300) < Math.abs(b.max_width - 300) ? -1 : 1; + } else if (a.format !== b.format) { + let a_index = format_preferences.indexOf(a.format); + if (a_index === -1) { + a_index = format_preferences.length; + } + let b_index = format_preferences.indexOf(b.format); + if (b_index === -1) { + b_index = format_preferences.length; + } + return a_index - b_index; + } + + return 0; + }); + preferred_format = sorted_formats.find((format) => !format.animated)!; + animated_format = sorted_formats.find((format) => format.animated)!; +} diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 0977188494..8ba30d9789 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -126,6 +126,7 @@ import * as stream_topic_history from "./stream_topic_history"; import * as stream_topic_history_util from "./stream_topic_history_util"; import * as sub_store from "./sub_store"; import * as theme from "./theme"; +import * as thumbnail from "./thumbnail"; import * as timerender from "./timerender"; import * as tippyjs from "./tippyjs"; import * as topic_list from "./topic_list"; @@ -426,6 +427,7 @@ export function initialize_everything(state_data) { if (page_params.is_spectator) { theme.initialize_theme_for_spectator(); } + thumbnail.initialize(); widgets.initialize(); tippyjs.initialize(); compose_tooltips.initialize(); diff --git a/zerver/lib/events.py b/zerver/lib/events.py index e7db80a8fc..37ac40ce1a 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -54,6 +54,7 @@ from zerver.lib.subscription_info import ( gather_subscriptions_helper, get_web_public_subs, ) +from zerver.lib.thumbnail import THUMBNAIL_OUTPUT_FORMATS from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timezone import canonicalize_timezone from zerver.lib.topic import TOPIC_NAME @@ -381,6 +382,16 @@ def fetch_initial_state_data( state["password_min_guesses"] = settings.PASSWORD_MIN_GUESSES state["server_inline_image_preview"] = settings.INLINE_IMAGE_PREVIEW state["server_inline_url_embed_preview"] = settings.INLINE_URL_EMBED_PREVIEW + state["server_thumbnail_formats"] = [ + { + "name": str(thumbnail_format), + "max_width": thumbnail_format.max_width, + "max_height": thumbnail_format.max_height, + "format": thumbnail_format.extension, + "animated": thumbnail_format.animated, + } + for thumbnail_format in THUMBNAIL_OUTPUT_FORMATS + ] state["server_avatar_changes_disabled"] = settings.AVATAR_CHANGES_DISABLED state["server_name_changes_disabled"] = settings.NAME_CHANGES_DISABLED state["server_web_public_streams_enabled"] = settings.WEB_PUBLIC_STREAMS_ENABLED diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index db6ed61f99..1dba7b7364 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -16397,6 +16397,43 @@ paths: Clients containing administrative UI for changing `realm_inline_url_embed_preview` should consult this field before offering that feature. + server_thumbnail_formats: + description: | + A list describing the image formats that uploaded + images will be thumbnailed into. Any image with a + source starting with `/user_uploads/thumbnail/` can + have its last path component replaced with any of the + names contained in this list, to obtain the desired + thumbnail size. + + **Changes**: New in Zulip 9.0 (feature level 273). + type: array + items: + type: object + additionalProperties: false + properties: + name: + type: string + description: | + The file path component of the thumbnail format. + max_width: + type: integer + description: | + The maximum width of this format. + max_height: + type: integer + description: | + The maximum height of this format. + format: + type: string + description: | + The extension of this format. + animated: + type: boolean + description: | + If this file format is animated. These formats + are only generated for uploaded imates which + themselves are animated. server_avatar_changes_disabled: type: boolean description: | diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index d2bbdd6a0b..cdf38f4f87 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -219,6 +219,7 @@ class HomeTest(ZulipTestCase): "server_presence_offline_threshold_seconds", "server_presence_ping_interval_seconds", "server_supported_permission_settings", + "server_thumbnail_formats", "server_timestamp", "server_typing_started_expiry_period_milliseconds", "server_typing_started_wait_period_milliseconds",