diff --git a/package.json b/package.json index 8bcda024cc..b631e1b6cd 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "@babel/eslint-parser": "^7.11.3", "@babel/plugin-transform-modules-commonjs": "^7.19.6", "@formatjs/cli": "^6.0.0", + "@giphy/js-types": "^5.1.0", "@types/autosize": "^4.0.1", "@types/blueimp-md5": "^2.18.0", "@types/co-body": "^6.1.3", @@ -116,6 +117,7 @@ "@types/sortablejs": "^1.15.1", "@types/spectrum": "^1.8.4", "@types/textarea-caret": "^3.0.3", + "@types/throttle-debounce": "^5.0.2", "@types/tinycolor2": "^1.4.5", "@types/turndown": "^5.0.1", "@typescript-eslint/eslint-plugin": "^8.2.0", @@ -141,6 +143,7 @@ "mockdate": "^3.0.2", "nyc": "^17.0.0", "openapi-examples-validator": "^5.0.0", + "preact": "^10.24.3", "prettier": "^3.0.0", "puppeteer": "^23.5.0", "source-map": "npm:source-map-js@^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4966338822..a92ad18271 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,6 +305,9 @@ importers: '@formatjs/cli': specifier: ^6.0.0 version: 6.3.8 + '@giphy/js-types': + specifier: ^5.1.0 + version: 5.1.0 '@types/autosize': specifier: ^4.0.1 version: 4.0.3 @@ -359,6 +362,9 @@ importers: '@types/textarea-caret': specifier: ^3.0.3 version: 3.0.3 + '@types/throttle-debounce': + specifier: ^5.0.2 + version: 5.0.2 '@types/tinycolor2': specifier: ^1.4.5 version: 1.4.6 @@ -434,6 +440,9 @@ importers: openapi-examples-validator: specifier: ^5.0.0 version: 5.0.0 + preact: + specifier: ^10.24.3 + version: 10.24.3 prettier: specifier: ^3.0.0 version: 3.3.3 @@ -2481,6 +2490,9 @@ packages: '@types/textarea-caret@3.0.3': resolution: {integrity: sha512-bsA9GdXV1wQsXyDjS5+A+czz8IAR3haH5DU+KctIoXbzobRL2NOiwF/+EbB7pofAyudMytLj4ihPtbmbJT8FWw==} + '@types/throttle-debounce@5.0.2': + resolution: {integrity: sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==} + '@types/tinycolor2@1.4.6': resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} @@ -11322,6 +11334,8 @@ snapshots: '@types/textarea-caret@3.0.3': {} + '@types/throttle-debounce@5.0.2': {} + '@types/tinycolor2@1.4.6': {} '@types/triple-beam@1.3.5': {} diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 7384eee97b..6d864a9f12 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -109,7 +109,7 @@ EXEMPT_FILES = make_set( "web/src/fetch_status.ts", "web/src/flatpickr.ts", "web/src/gear_menu.js", - "web/src/giphy.js", + "web/src/giphy.ts", "web/src/giphy_state.ts", "web/src/global.ts", "web/src/group_setting_pill.ts", diff --git a/version.py b/version.py index fd5e9c3abb..091ac31bce 100644 --- a/version.py +++ b/version.py @@ -49,4 +49,4 @@ API_FEATURE_LEVEL = 319 # Last bumped for message-link class # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = (301, 2) # bumped 2024-11-12 to patch handlebars +PROVISION_VERSION = (301, 3) # bumped 2024-11-13 for @giphy/js-types diff --git a/web/src/giphy.js b/web/src/giphy.ts similarity index 69% rename from web/src/giphy.js rename to web/src/giphy.ts index 79444dd1b7..172380ad94 100644 --- a/web/src/giphy.js +++ b/web/src/giphy.ts @@ -1,5 +1,9 @@ +import type {GifsResult, GiphyFetch, Rating} from "@giphy/js-fetch-api"; +import type {IGif} from "@giphy/js-types"; import $ from "jquery"; import _ from "lodash"; +import assert from "minimalistic-assert"; +import type * as tippy from "tippy.js"; import render_giphy_picker from "../templates/giphy_picker.hbs"; @@ -9,24 +13,26 @@ import * as popover_menus from "./popover_menus.ts"; import * as rows from "./rows.ts"; import {realm} from "./state_data.ts"; import * as ui_util from "./ui_util.ts"; +import {the} from "./util.ts"; -let giphy_fetch; +let giphy_fetch: GiphyFetch | undefined; let search_term = ""; -let gifs_grid; -let giphy_popover_instance = null; +let gifs_grid: {remove: () => void} | undefined; +let giphy_popover_instance: tippy.Instance | undefined; // Only used if popover called from edit message, otherwise it is `undefined`. -let edit_message_id; +let edit_message_id: number | undefined; -export function is_popped_from_edit_message() { - return giphy_popover_instance && edit_message_id !== undefined; +export function is_popped_from_edit_message(): boolean { + return giphy_popover_instance !== undefined && edit_message_id !== undefined; } -export function focus_current_edit_message() { - $(`#edit_form_${CSS.escape(edit_message_id)} .message_edit_content`).trigger("focus"); +export function focus_current_edit_message(): void { + assert(edit_message_id !== undefined); + $(`#edit_form_${CSS.escape(`${edit_message_id}`)} .message_edit_content`).trigger("focus"); } -export function update_giphy_rating() { +export function update_giphy_rating(): void { if ( realm.realm_giphy_rating === realm.giphy_rating_options.disabled.id || realm.giphy_api_key === "" @@ -37,10 +43,10 @@ export function update_giphy_rating() { } } -function get_rating() { +function get_rating(): Rating { const options = realm.giphy_rating_options; - for (const rating in realm.giphy_rating_options) { - if (options[rating].id === realm.realm_giphy_rating) { + for (const rating of ["pg", "g", "y", "pg-13", "r"] as const) { + if (options[rating]?.id === realm.realm_giphy_rating) { return rating; } } @@ -51,7 +57,7 @@ function get_rating() { return "g"; } -async function renderGIPHYGrid(targetEl) { +async function renderGIPHYGrid(targetEl: HTMLElement): Promise<{remove: () => void}> { const {renderGrid} = await import(/* webpackChunkName: "giphy-sdk" */ "@giphy/js-components"); const {GiphyFetch} = await import(/* webpackChunkName: "giphy-sdk" */ "@giphy/js-fetch-api"); @@ -59,7 +65,8 @@ async function renderGIPHYGrid(targetEl) { giphy_fetch = new GiphyFetch(realm.giphy_api_key); } - function fetchGifs(offset) { + async function fetchGifs(offset: number): Promise { + assert(giphy_fetch !== undefined); const config = { offset, limit: 25, @@ -73,7 +80,7 @@ async function renderGIPHYGrid(targetEl) { return giphy_fetch.search(search_term, config); } - const render = () => + const render = (): (() => void) => // See https://github.com/Giphy/giphy-js/blob/master/packages/components/README.md#grid // for detailed documentation. renderGrid( @@ -86,11 +93,11 @@ async function renderGIPHYGrid(targetEl) { // Hide the creator attribution that appears over a // GIF; nice in principle but too distracting. hideAttribution: true, - onGifClick(props) { - let $textarea = $("textarea#compose-textarea"); + onGifClick(props: IGif) { + let $textarea = $("textarea#compose-textarea"); if (edit_message_id !== undefined) { $textarea = $( - `#edit_form_${CSS.escape(edit_message_id)} .message_edit_content`, + `#edit_form_${CSS.escape(`${edit_message_id}`)} .message_edit_content`, ); } @@ -120,18 +127,18 @@ async function renderGIPHYGrid(targetEl) { }; } -async function update_grid_with_search_term() { +async function update_grid_with_search_term(): Promise { if (!gifs_grid) { return; } - const $search_elem = $("#giphy-search-query"); + const $search_elem = $("input#giphy-search-query"); // GIPHY popover may have been hidden by the // time this function is called. if ($search_elem.length) { - search_term = $search_elem[0].value; + search_term = the($search_elem).value; gifs_grid.remove(); - gifs_grid = await renderGIPHYGrid($("#giphy_grid_in_popover .giphy-content")[0]); + gifs_grid = await renderGIPHYGrid(the($("#giphy_grid_in_popover .giphy-content"))); return; } @@ -139,7 +146,7 @@ async function update_grid_with_search_term() { gifs_grid = undefined; } -export function hide_giphy_popover() { +export function hide_giphy_popover(): boolean { // Returns `true` if the popover was open. if (giphy_popover_instance) { giphy_popover_instance.destroy(); @@ -151,7 +158,7 @@ export function hide_giphy_popover() { return false; } -function toggle_giphy_popover(target) { +function toggle_giphy_popover(target: HTMLElement): void { popover_menus.toggle_popover_menu( target, { @@ -161,10 +168,9 @@ function toggle_giphy_popover(target) { instance.setContent(ui_util.parse_html(render_giphy_picker())); $(instance.popper).addClass("giphy-popover"); }, - async onShow(instance) { + onShow(instance) { giphy_popover_instance = instance; const $popper = $(giphy_popover_instance.popper).trigger("focus"); - gifs_grid = await renderGIPHYGrid($popper.find(".giphy-content")[0]); const $click_target = $(instance.reference); if ($click_target.parents(".message_edit_form").length === 1) { @@ -186,22 +192,26 @@ function toggle_giphy_popover(target) { // every search. This makes the UX of searching pleasant // by allowing user to finish typing before search // is executed. - _.debounce(update_grid_with_search_term, 300), + _.debounce(() => void update_grid_with_search_term(), 300), ); $popper.on("keydown", ".giphy-gif", ui_util.convert_enter_to_click); $popper.on("keydown", ".compose_gif_icon", ui_util.convert_enter_to_click); - $popper.on("click", "#giphy_search_clear", async (e) => { + $popper.on("click", "#giphy_search_clear", (e) => { e.stopPropagation(); $("#giphy-search-query").val(""); - await update_grid_with_search_term(); + void update_grid_with_search_term(); }); - // Focus on search box by default. - // This is specially helpful for users - // navigating via keyboard. - $("#giphy-search-query").trigger("focus"); + void (async () => { + gifs_grid = await renderGIPHYGrid(the($popper.find(".giphy-content"))); + + // Focus on search box by default. + // This is specially helpful for users + // navigating via keyboard. + $("#giphy-search-query").trigger("focus"); + })(); }, onHidden() { hide_giphy_popover(); @@ -214,12 +224,12 @@ function toggle_giphy_popover(target) { ); } -function register_click_handlers() { - $("body").on("click", ".compose_control_button.compose_gif_icon", (e) => { - toggle_giphy_popover(e.currentTarget); +function register_click_handlers(): void { + $("body").on("click", ".compose_control_button.compose_gif_icon", function (this: HTMLElement) { + toggle_giphy_popover(this); }); } -export function initialize() { +export function initialize(): void { register_click_handlers(); } diff --git a/web/src/hotkey.js b/web/src/hotkey.js index cba3ab470f..0e9e3fd0af 100644 --- a/web/src/hotkey.js +++ b/web/src/hotkey.js @@ -21,7 +21,7 @@ import * as emoji from "./emoji.ts"; import * as emoji_picker from "./emoji_picker.ts"; import * as feedback_widget from "./feedback_widget.ts"; import * as gear_menu from "./gear_menu.js"; -import * as giphy from "./giphy.js"; +import * as giphy from "./giphy.ts"; import * as hash_util from "./hash_util.ts"; import * as hashchange from "./hashchange.js"; import * as inbox_ui from "./inbox_ui.ts"; diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index 1d0ee10a65..ca091ad510 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -20,7 +20,7 @@ import {electron_bridge} from "./electron_bridge.ts"; import * as emoji from "./emoji.ts"; import * as emoji_picker from "./emoji_picker.ts"; import * as gear_menu from "./gear_menu.js"; -import * as giphy from "./giphy.js"; +import * as giphy from "./giphy.ts"; import * as information_density from "./information_density.ts"; import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area.ts"; import * as linkifiers from "./linkifiers.ts"; diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 7aa343002a..e45bbfd200 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -44,7 +44,7 @@ import * as emoji from "./emoji.ts"; import * as emoji_picker from "./emoji_picker.ts"; import * as emojisets from "./emojisets.ts"; import * as gear_menu from "./gear_menu.js"; -import * as giphy from "./giphy.js"; +import * as giphy from "./giphy.ts"; import * as giphy_state from "./giphy_state.ts"; import * as hashchange from "./hashchange.js"; import * as hotkey from "./hotkey.js";