diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a721d7fb..36a14494 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,6 +37,7 @@ jobs: env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + VITE_KLIPY_API_KEY: ${{ secrets.VITE_KLIPY_API_KEY }} run: npm run tauri build - name: Stage Windows release assets diff --git a/Client/tauri-client/.gitignore b/Client/tauri-client/.gitignore index 1ed2749a..13cbd141 100644 --- a/Client/tauri-client/.gitignore +++ b/Client/tauri-client/.gitignore @@ -7,4 +7,5 @@ src-tauri/gen/ coverage/ playwright-report/ test-results/ -~/ \ No newline at end of file +~/ +.env \ No newline at end of file diff --git a/Client/tauri-client/src/assets/KLIPY Light with logo.svg b/Client/tauri-client/src/assets/KLIPY Light with logo.svg new file mode 100644 index 00000000..756df803 --- /dev/null +++ b/Client/tauri-client/src/assets/KLIPY Light with logo.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Client/tauri-client/src/components/GifPicker.ts b/Client/tauri-client/src/components/GifPicker.ts index 5538b6c3..f73445e7 100644 --- a/Client/tauri-client/src/components/GifPicker.ts +++ b/Client/tauri-client/src/components/GifPicker.ts @@ -1,9 +1,9 @@ -// GifPicker — searchable GIF selector powered by Tenor API. +// GifPicker — searchable GIF selector powered by Klipy API. // Uses @lib/dom helpers exclusively. Never sets innerHTML with user content. import { createElement, setText, clearChildren } from "@lib/dom"; -import { searchGifs, getTrendingGifs } from "@lib/tenor"; -import type { TenorGif } from "@lib/tenor"; +import { searchGifs, getTrendingGifs } from "@lib/gifProvider"; +import type { GifResult } from "@lib/gifProvider"; // --------------------------------------------------------------------------- // Types @@ -43,13 +43,13 @@ export function createGifPicker(options: GifPickerOptions): { const searchInput = createElement("input", { class: "gp-search", type: "text", - placeholder: "Search Tenor", + placeholder: "Search Klipy", }); header.appendChild(searchInput); // Attribution const attribution = createElement("div", { class: "gp-attribution" }); - setText(attribution, "Powered by Tenor"); + setText(attribution, "Powered by Klipy"); header.appendChild(attribution); root.appendChild(header); @@ -68,7 +68,7 @@ export function createGifPicker(options: GifPickerOptions): { // ── Rendering ── - function renderGifs(gifs: readonly TenorGif[]): void { + function renderGifs(gifs: readonly GifResult[]): void { clearChildren(gridArea); if (gifs.length === 0) { diff --git a/Client/tauri-client/src/components/message-list/media.ts b/Client/tauri-client/src/components/message-list/media.ts index 4ced9824..5b6f774a 100644 --- a/Client/tauri-client/src/components/message-list/media.ts +++ b/Client/tauri-client/src/components/message-list/media.ts @@ -5,6 +5,7 @@ import { createElement, setText, appendChildren } from "@lib/dom"; import { createIcon } from "@lib/icons"; +import klipyWatermark from "../../assets/KLIPY Light with logo.svg"; import { createLogger } from "@lib/logger"; import { observeMedia } from "@lib/media-visibility"; import { loadPref } from "@components/settings/helpers"; @@ -34,6 +35,16 @@ function cacheImageHeight(url: string, h: number): void { imageHeightCache.set(url, h); } +/** Check if a URL originates from the Klipy CDN. */ +function isKlipyUrl(url: string): boolean { + try { + const { hostname } = new URL(url); + return hostname === "klipy.com" || hostname.endsWith(".klipy.com"); + } catch { + return false; + } +} + /** Check if a URL points to an animated GIF. */ function isGifUrl(url: string): boolean { try { @@ -289,6 +300,17 @@ export function renderInlineImage(url: string): HTMLDivElement { }); wrap.appendChild(img); + + if (isKlipyUrl(url)) { + const watermark = createElement("img", { + class: "klipy-watermark", + src: klipyWatermark, + alt: "", + "aria-hidden": "true", + }); + wrap.appendChild(watermark); + } + return wrap; } diff --git a/Client/tauri-client/src/lib/gifProvider.ts b/Client/tauri-client/src/lib/gifProvider.ts new file mode 100644 index 00000000..82f54a28 --- /dev/null +++ b/Client/tauri-client/src/lib/gifProvider.ts @@ -0,0 +1,115 @@ +// Klipy GIF API client — provides GIF search and trending. +// Drop-in replacement for Tenor (EOL June 30, 2026). +// Register at partner.klipy.com to get a production API key. +// Override via VITE_KLIPY_API_KEY at build time. +const KLIPY_API_KEY = import.meta.env.VITE_KLIPY_API_KEY ?? ""; +const GIF_API_BASE = "https://api.klipy.com/v2"; +const DEFAULT_LIMIT = 20; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface GifResult { + readonly id: string; + readonly title: string; + /** tinygif URL for preview thumbnails */ + readonly url: string; + /** Full-size gif URL for sending */ + readonly fullUrl: string; +} + +interface GifMediaFormat { + readonly url: string; +} + +interface GifApiResult { + readonly id: string; + readonly title: string; + readonly media_formats: { + readonly tinygif?: GifMediaFormat; + readonly gif?: GifMediaFormat; + }; +} + +interface GifSearchResponse { + readonly results: readonly GifApiResult[]; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Validate that a URL originates from a trusted Klipy CDN domain. */ +function isAllowedGifUrl(url: string): boolean { + try { + const parsed = new URL(url); + return ( + parsed.protocol === "https:" && + (parsed.hostname === "klipy.com" || parsed.hostname.endsWith(".klipy.com")) + ); + } catch { + return false; + } +} + +function parseResults(data: GifSearchResponse): readonly GifResult[] { + return data.results + .filter((r) => { + const tinyUrl = r.media_formats.tinygif?.url ?? ""; + const gifUrl = r.media_formats.gif?.url ?? ""; + return tinyUrl && gifUrl && isAllowedGifUrl(tinyUrl) && isAllowedGifUrl(gifUrl); + }) + .map((r) => ({ + id: r.id, + title: r.title, + url: r.media_formats.tinygif!.url, + fullUrl: r.media_formats.gif!.url, + })); +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Search Klipy for GIFs matching the given query. + */ +export async function searchGifs( + query: string, + limit: number = DEFAULT_LIMIT, +): Promise { + const params = new URLSearchParams({ + q: query, + key: KLIPY_API_KEY, + limit: String(limit), + media_filter: "gif,tinygif", + }); + + const res = await fetch(`${GIF_API_BASE}/search?${params.toString()}`); + if (!res.ok) { + throw new Error(`GIF search failed: ${res.status} ${res.statusText}`); + } + const data: GifSearchResponse = await res.json(); + return parseResults(data); +} + +/** + * Fetch currently trending GIFs from Klipy. + */ +export async function getTrendingGifs( + limit: number = DEFAULT_LIMIT, +): Promise { + const params = new URLSearchParams({ + key: KLIPY_API_KEY, + limit: String(limit), + media_filter: "gif,tinygif", + }); + + const res = await fetch(`${GIF_API_BASE}/featured?${params.toString()}`); + if (!res.ok) { + throw new Error(`GIF trending failed: ${res.status} ${res.statusText}`); + } + const data: GifSearchResponse = await res.json(); + return parseResults(data); +} diff --git a/Client/tauri-client/src/lib/tenor.ts b/Client/tauri-client/src/lib/tenor.ts deleted file mode 100644 index 3b6b453c..00000000 --- a/Client/tauri-client/src/lib/tenor.ts +++ /dev/null @@ -1,122 +0,0 @@ -// Tenor API v2 client — provides GIF search and trending. -// Uses the anonymous test key for development. - -// Tenor API key — defaults to Google's public anonymous test key from -// https://developers.google.com/tenor/guides/quickstart -// This key is intentionally public (Google's demo key, not a secret). -// Override via VITE_TENOR_API_KEY at build time for production use. -const TENOR_API_KEY = - // codeql[js/hardcoded-credentials] -- Google's public anonymous demo key, not a secret - import.meta.env.VITE_TENOR_API_KEY ?? "AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ"; -const TENOR_BASE = "https://tenor.googleapis.com/v2"; -const DEFAULT_LIMIT = 20; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export interface TenorGif { - readonly id: string; - readonly title: string; - /** tinygif URL for preview thumbnails */ - readonly url: string; - /** Full-size gif URL for sending */ - readonly fullUrl: string; -} - -interface TenorMediaFormat { - readonly url: string; -} - -interface TenorResult { - readonly id: string; - readonly title: string; - readonly media_formats: { - readonly tinygif?: TenorMediaFormat; - readonly gif?: TenorMediaFormat; - }; -} - -interface TenorResponse { - readonly results: readonly TenorResult[]; -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Trusted Tenor CDN origins for URL validation. */ -const TENOR_ALLOWED_ORIGINS = new Set([ - "https://media.tenor.com", - "https://c.tenor.com", - "https://media1.tenor.com", -]); - -/** Validate that a URL originates from a trusted Tenor domain. */ -function isTenorUrl(url: string): boolean { - try { - const parsed = new URL(url); - return TENOR_ALLOWED_ORIGINS.has(parsed.origin) && parsed.protocol === "https:"; - } catch { - return false; - } -} - -function parseResults(data: TenorResponse): readonly TenorGif[] { - return data.results - .filter((r) => { - const tinyUrl = r.media_formats.tinygif?.url ?? ""; - const gifUrl = r.media_formats.gif?.url ?? ""; - return tinyUrl && gifUrl && isTenorUrl(tinyUrl) && isTenorUrl(gifUrl); - }) - .map((r) => ({ - id: r.id, - title: r.title, - url: r.media_formats.tinygif!.url, - fullUrl: r.media_formats.gif!.url, - })); -} - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -/** - * Search Tenor for GIFs matching the given query. - */ -export async function searchGifs( - query: string, - limit: number = DEFAULT_LIMIT, -): Promise { - const params = new URLSearchParams({ - q: query, - key: TENOR_API_KEY, - limit: String(limit), - media_filter: "gif,tinygif", - }); - - const res = await fetch(`${TENOR_BASE}/search?${params.toString()}`); - if (!res.ok) { - throw new Error(`Tenor search failed: ${res.status} ${res.statusText}`); - } - const data: TenorResponse = await res.json(); - return parseResults(data); -} - -/** - * Fetch currently trending GIFs from Tenor. - */ -export async function getTrendingGifs(limit: number = DEFAULT_LIMIT): Promise { - const params = new URLSearchParams({ - key: TENOR_API_KEY, - limit: String(limit), - media_filter: "gif,tinygif", - }); - - const res = await fetch(`${TENOR_BASE}/featured?${params.toString()}`); - if (!res.ok) { - throw new Error(`Tenor trending failed: ${res.status} ${res.statusText}`); - } - const data: TenorResponse = await res.json(); - return parseResults(data); -} diff --git a/Client/tauri-client/src/styles/app.css b/Client/tauri-client/src/styles/app.css index d42fda92..f35e55f0 100644 --- a/Client/tauri-client/src/styles/app.css +++ b/Client/tauri-client/src/styles/app.css @@ -642,7 +642,7 @@ /* Image attachment */ .msg-image { - margin-top: 4px; max-width: 550px; border-radius: var(--radius-md); + position: relative; margin-top: 4px; max-width: 550px; border-radius: var(--radius-md); overflow: hidden; cursor: pointer; contain: layout style; } .msg-image img { @@ -1106,10 +1106,15 @@ gap: 6px; } .gp-item { - cursor: pointer; border-radius: var(--radius-sm); + position: relative; cursor: pointer; border-radius: var(--radius-sm); overflow: hidden; transition: transform .1s; } .gp-item:hover { transform: scale(1.03); } +.klipy-watermark { + position: absolute; bottom: 6px; left: 6px; + height: 18px; width: auto; pointer-events: none; + opacity: 0.9; +} .gp-img { width: 100%; display: block; object-fit: cover; border-radius: var(--radius-sm); diff --git a/Client/tauri-client/tests/unit/gif-picker.test.ts b/Client/tauri-client/tests/unit/gif-picker.test.ts index 1c24349d..0fc16fa3 100644 --- a/Client/tauri-client/tests/unit/gif-picker.test.ts +++ b/Client/tauri-client/tests/unit/gif-picker.test.ts @@ -1,36 +1,36 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { createGifPicker } from "@components/GifPicker"; import type { GifPickerOptions } from "@components/GifPicker"; -import type { TenorGif } from "@lib/tenor"; +import type { GifResult } from "@lib/gifProvider"; // --------------------------------------------------------------------------- // Module mock — must be hoisted before imports in vitest // --------------------------------------------------------------------------- -vi.mock("@lib/tenor", () => ({ +vi.mock("@lib/gifProvider", () => ({ searchGifs: vi.fn(), getTrendingGifs: vi.fn(), })); // Import the mocks so tests can control their return values -import { searchGifs, getTrendingGifs } from "@lib/tenor"; +import { searchGifs, getTrendingGifs } from "@lib/gifProvider"; // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- -function makeGif(id: string): TenorGif { +function makeGif(id: string): GifResult { return { id, title: `GIF ${id}`, - url: `https://media.tenor.com/preview/${id}.gif`, - fullUrl: `https://media.tenor.com/full/${id}.gif`, + url: `https://media.klipy.com/preview/${id}.gif`, + fullUrl: `https://media.klipy.com/full/${id}.gif`, }; } -const TRENDING_GIFS: readonly TenorGif[] = [makeGif("t1"), makeGif("t2"), makeGif("t3")]; +const TRENDING_GIFS: readonly GifResult[] = [makeGif("t1"), makeGif("t2"), makeGif("t3")]; -const SEARCH_GIFS: readonly TenorGif[] = [makeGif("s1"), makeGif("s2")]; +const SEARCH_GIFS: readonly GifResult[] = [makeGif("s1"), makeGif("s2")]; // --------------------------------------------------------------------------- // Helpers @@ -90,7 +90,7 @@ describe("GifPicker", () => { const { picker } = makePicker(); const attribution = picker.element.querySelector(".gp-attribution"); expect(attribution).not.toBeNull(); - expect(attribution!.textContent).toBe("Powered by Tenor"); + expect(attribution!.textContent).toBe("Powered by Klipy"); picker.destroy(); }); }); @@ -103,7 +103,7 @@ describe("GifPicker", () => { const input = picker.element.querySelector(".gp-search") as HTMLInputElement; expect(input).not.toBeNull(); expect(input.tagName).toBe("INPUT"); - expect(input.placeholder).toBe("Search Tenor"); + expect(input.placeholder).toBe("Search Klipy"); picker.destroy(); }); @@ -265,11 +265,11 @@ describe("GifPicker", () => { }); it("img alt falls back to 'GIF' when title is empty", async () => { - const gifNoTitle: TenorGif = { + const gifNoTitle: GifResult = { id: "no-title", title: "", - url: "https://media.tenor.com/preview/no-title.gif", - fullUrl: "https://media.tenor.com/full/no-title.gif", + url: "https://media.klipy.com/preview/no-title.gif", + fullUrl: "https://media.klipy.com/full/no-title.gif", }; vi.mocked(getTrendingGifs).mockResolvedValue([gifNoTitle]); diff --git a/Client/tauri-client/tests/unit/tenor.test.ts b/Client/tauri-client/tests/unit/gif-provider.test.ts similarity index 80% rename from Client/tauri-client/tests/unit/tenor.test.ts rename to Client/tauri-client/tests/unit/gif-provider.test.ts index 0891b4a3..67b398ef 100644 --- a/Client/tauri-client/tests/unit/tenor.test.ts +++ b/Client/tauri-client/tests/unit/gif-provider.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { searchGifs, getTrendingGifs } from "../../src/lib/tenor"; +import { searchGifs, getTrendingGifs } from "../../src/lib/gifProvider"; // --------------------------------------------------------------------------- -// Fetch mock — global fetch used by tenor.ts (no plugin wrapper) +// Fetch mock — global fetch used by gifProvider.ts (no plugin wrapper) // --------------------------------------------------------------------------- const mockFetch = vi.fn(); @@ -20,7 +20,7 @@ afterEach(() => { // Helpers // --------------------------------------------------------------------------- -function tenorResult( +function gifResult( id: string, overrides: { tinygif?: string | null; @@ -29,8 +29,8 @@ function tenorResult( } = {}, ) { const { - tinygif = `https://media.tenor.com/${id}_tiny.gif`, - gif = `https://media.tenor.com/${id}.gif`, + tinygif = `https://media.klipy.com/${id}_tiny.gif`, + gif = `https://media.klipy.com/${id}.gif`, title = `Title ${id}`, } = overrides; @@ -75,10 +75,10 @@ function capturedUrl(): string { describe("searchGifs", () => { describe("URL construction", () => { - it("calls the Tenor search endpoint", async () => { + it("calls the Klipy search endpoint", async () => { mockFetch.mockResolvedValue(okResponse([])); await searchGifs("cats"); - expect(capturedUrl()).toMatch(/^https:\/\/tenor\.googleapis\.com\/v2\/search/); + expect(capturedUrl()).toMatch(/^https:\/\/api\.klipy\.com\/v2\/search/); }); it("includes the query param q", async () => { @@ -90,7 +90,7 @@ describe("searchGifs", () => { it("includes the API key param", async () => { mockFetch.mockResolvedValue(okResponse([])); await searchGifs("dogs"); - expect(capturedParams().get("key")).toBeTruthy(); + expect(capturedParams().has("key")).toBe(true); }); it("includes media_filter param", async () => { @@ -127,20 +127,20 @@ describe("searchGifs", () => { }); it("maps id, title, url (tinygif), and fullUrl (gif) correctly", async () => { - mockFetch.mockResolvedValue(okResponse([tenorResult("abc123")])); + mockFetch.mockResolvedValue(okResponse([gifResult("abc123")])); const gifs = await searchGifs("cats"); expect(gifs).toHaveLength(1); expect(gifs[0]).toEqual({ id: "abc123", title: "Title abc123", - url: "https://media.tenor.com/abc123_tiny.gif", - fullUrl: "https://media.tenor.com/abc123.gif", + url: "https://media.klipy.com/abc123_tiny.gif", + fullUrl: "https://media.klipy.com/abc123.gif", }); }); it("maps multiple results in order", async () => { mockFetch.mockResolvedValue( - okResponse([tenorResult("a"), tenorResult("b"), tenorResult("c")]), + okResponse([gifResult("a"), gifResult("b"), gifResult("c")]), ); const gifs = await searchGifs("cats"); expect(gifs.map((g) => g.id)).toEqual(["a", "b", "c"]); @@ -148,7 +148,7 @@ describe("searchGifs", () => { it("filters out results with no tinygif format", async () => { mockFetch.mockResolvedValue( - okResponse([tenorResult("keep"), tenorResult("drop", { tinygif: null })]), + okResponse([gifResult("keep"), gifResult("drop", { tinygif: null })]), ); const gifs = await searchGifs("cats"); expect(gifs).toHaveLength(1); @@ -157,7 +157,7 @@ describe("searchGifs", () => { it("filters out results with no gif format", async () => { mockFetch.mockResolvedValue( - okResponse([tenorResult("keep"), tenorResult("drop", { gif: null })]), + okResponse([gifResult("keep"), gifResult("drop", { gif: null })]), ); const gifs = await searchGifs("cats"); expect(gifs).toHaveLength(1); @@ -166,7 +166,7 @@ describe("searchGifs", () => { it("filters out results missing both formats", async () => { mockFetch.mockResolvedValue( - okResponse([tenorResult("drop", { tinygif: null, gif: null }), tenorResult("keep")]), + okResponse([gifResult("drop", { tinygif: null, gif: null }), gifResult("keep")]), ); const gifs = await searchGifs("cats"); expect(gifs).toHaveLength(1); @@ -175,11 +175,29 @@ describe("searchGifs", () => { it("returns an empty array when all results lack required formats", async () => { mockFetch.mockResolvedValue( - okResponse([tenorResult("x", { tinygif: null }), tenorResult("y", { gif: null })]), + okResponse([gifResult("x", { tinygif: null }), gifResult("y", { gif: null })]), ); const gifs = await searchGifs("cats"); expect(gifs).toEqual([]); }); + + it("filters out results with non-Klipy CDN URLs", async () => { + mockFetch.mockResolvedValue( + okResponse([ + gifResult("drop", { + tinygif: "https://media.tenor.com/drop_tiny.gif", + gif: "https://media.tenor.com/drop.gif", + }), + gifResult("keep", { + tinygif: "https://static.klipy.com/keep_tiny.gif", + gif: "https://static.klipy.com/keep.gif", + }), + ]), + ); + const gifs = await searchGifs("cats"); + expect(gifs).toHaveLength(1); + expect(gifs[0]?.id).toBe("keep"); + }); }); describe("HTTP error handling", () => { @@ -211,10 +229,10 @@ describe("searchGifs", () => { describe("getTrendingGifs", () => { describe("URL construction", () => { - it("calls the Tenor featured endpoint", async () => { + it("calls the Klipy featured endpoint", async () => { mockFetch.mockResolvedValue(okResponse([])); await getTrendingGifs(); - expect(capturedUrl()).toMatch(/^https:\/\/tenor\.googleapis\.com\/v2\/featured/); + expect(capturedUrl()).toMatch(/^https:\/\/api\.klipy\.com\/v2\/featured/); }); it("does not include a q param", async () => { @@ -226,7 +244,7 @@ describe("getTrendingGifs", () => { it("includes the API key param", async () => { mockFetch.mockResolvedValue(okResponse([])); await getTrendingGifs(); - expect(capturedParams().get("key")).toBeTruthy(); + expect(capturedParams().has("key")).toBe(true); }); it("includes media_filter param", async () => { @@ -256,19 +274,19 @@ describe("getTrendingGifs", () => { }); it("maps fields correctly", async () => { - mockFetch.mockResolvedValue(okResponse([tenorResult("trend1")])); + mockFetch.mockResolvedValue(okResponse([gifResult("trend1")])); const gifs = await getTrendingGifs(); expect(gifs[0]).toEqual({ id: "trend1", title: "Title trend1", - url: "https://media.tenor.com/trend1_tiny.gif", - fullUrl: "https://media.tenor.com/trend1.gif", + url: "https://media.klipy.com/trend1_tiny.gif", + fullUrl: "https://media.klipy.com/trend1.gif", }); }); it("filters out results with missing tinygif", async () => { mockFetch.mockResolvedValue( - okResponse([tenorResult("keep"), tenorResult("drop", { tinygif: null })]), + okResponse([gifResult("keep"), gifResult("drop", { tinygif: null })]), ); const gifs = await getTrendingGifs(); expect(gifs.map((g) => g.id)).toEqual(["keep"]); @@ -276,7 +294,7 @@ describe("getTrendingGifs", () => { it("filters out results with missing gif", async () => { mockFetch.mockResolvedValue( - okResponse([tenorResult("keep"), tenorResult("drop", { gif: null })]), + okResponse([gifResult("keep"), gifResult("drop", { gif: null })]), ); const gifs = await getTrendingGifs(); expect(gifs.map((g) => g.id)).toEqual(["keep"]); diff --git a/Client/tauri-client/tests/unit/message-input.test.ts b/Client/tauri-client/tests/unit/message-input.test.ts index db91e22a..6a4c6be0 100644 --- a/Client/tauri-client/tests/unit/message-input.test.ts +++ b/Client/tauri-client/tests/unit/message-input.test.ts @@ -746,9 +746,9 @@ describe("MessageInput", () => { gifBtn.click(); expect(lastGifPickerOptions).not.toBeNull(); - lastGifPickerOptions!.onSelect("https://tenor.com/example.gif"); + lastGifPickerOptions!.onSelect("https://media.klipy.com/example.gif"); - expect(opts.onSend).toHaveBeenCalledWith("https://tenor.com/example.gif", null, []); + expect(opts.onSend).toHaveBeenCalledWith("https://media.klipy.com/example.gif", null, []); comp.destroy?.(); });