fix: refine media attachment previews
This commit is contained in:
parent
2157fa769a
commit
0cedf8e14b
72
apps/api/internal/webassets/dist/assets/index-DimLK6Kw.js
vendored
Normal file
72
apps/api/internal/webassets/dist/assets/index-DimLK6Kw.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
apps/api/internal/webassets/dist/index.html
vendored
4
apps/api/internal/webassets/dist/index.html
vendored
@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ClickClack</title>
|
||||
<script type="module" crossorigin src="/assets/index-vInMpaH9.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BddjDjB3.css">
|
||||
<script type="module" crossorigin src="/assets/index-DimLK6Kw.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-u1LjbixA.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { onDestroy, onMount, tick } from "svelte";
|
||||
import { APIError, api } from "./lib/api";
|
||||
import { markdown, time } from "./lib/format";
|
||||
import MediaAttachment from "./components/MediaAttachment.svelte";
|
||||
import type { Channel, DirectConversation, Message, RealtimeEvent, SearchResult, ThreadState, Upload, User, Workspace } from "./lib/types";
|
||||
|
||||
let user: User | null = null;
|
||||
@ -1254,32 +1255,11 @@
|
||||
{#if message.attachments?.length}
|
||||
<div class="attachment-grid" aria-label="Attachments">
|
||||
{#each message.attachments as attachment (attachment.id)}
|
||||
{#if isImageUpload(attachment)}
|
||||
<button
|
||||
type="button"
|
||||
class="image-attachment"
|
||||
aria-label={`Open image ${attachment.filename}`}
|
||||
onclick={() => openImageViewer(uploadURL(attachment), attachment.filename)}
|
||||
>
|
||||
<img src={uploadURL(attachment)} alt={attachment.filename} loading="lazy" />
|
||||
<span>{attachment.filename}</span>
|
||||
</button>
|
||||
{:else if isVideoUpload(attachment)}
|
||||
<div class="video-attachment">
|
||||
<video controls preload="metadata" aria-label={attachment.filename}>
|
||||
<source src={uploadURL(attachment)} type={attachment.content_type} />
|
||||
</video>
|
||||
<a href={uploadURL(attachment)} target="_blank" rel="noreferrer">{attachment.filename}</a>
|
||||
</div>
|
||||
{:else}
|
||||
<a class="file-attachment" href={uploadURL(attachment)} target="_blank" rel="noreferrer">
|
||||
<span class="file-icon" aria-hidden="true">↧</span>
|
||||
<span>
|
||||
<strong>{attachment.filename}</strong>
|
||||
<small>{formatBytes(attachment.byte_size)}</small>
|
||||
</span>
|
||||
</a>
|
||||
{/if}
|
||||
<MediaAttachment
|
||||
upload={attachment}
|
||||
url={uploadURL(attachment)}
|
||||
onOpenImage={openImageViewer}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@ -1466,32 +1446,11 @@
|
||||
{#if selectedThread.attachments?.length}
|
||||
<div class="attachment-grid compact" aria-label="Attachments">
|
||||
{#each selectedThread.attachments as attachment (attachment.id)}
|
||||
{#if isImageUpload(attachment)}
|
||||
<button
|
||||
type="button"
|
||||
class="image-attachment"
|
||||
aria-label={`Open image ${attachment.filename}`}
|
||||
onclick={() => openImageViewer(uploadURL(attachment), attachment.filename)}
|
||||
>
|
||||
<img src={uploadURL(attachment)} alt={attachment.filename} loading="lazy" />
|
||||
<span>{attachment.filename}</span>
|
||||
</button>
|
||||
{:else if isVideoUpload(attachment)}
|
||||
<div class="video-attachment">
|
||||
<video controls preload="metadata" aria-label={attachment.filename}>
|
||||
<source src={uploadURL(attachment)} type={attachment.content_type} />
|
||||
</video>
|
||||
<a href={uploadURL(attachment)} target="_blank" rel="noreferrer">{attachment.filename}</a>
|
||||
</div>
|
||||
{:else}
|
||||
<a class="file-attachment" href={uploadURL(attachment)} target="_blank" rel="noreferrer">
|
||||
<span class="file-icon" aria-hidden="true">↧</span>
|
||||
<span>
|
||||
<strong>{attachment.filename}</strong>
|
||||
<small>{formatBytes(attachment.byte_size)}</small>
|
||||
</span>
|
||||
</a>
|
||||
{/if}
|
||||
<MediaAttachment
|
||||
upload={attachment}
|
||||
url={uploadURL(attachment)}
|
||||
onOpenImage={openImageViewer}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@ -1545,32 +1504,11 @@
|
||||
{#if reply.attachments?.length}
|
||||
<div class="attachment-grid compact" aria-label="Attachments">
|
||||
{#each reply.attachments as attachment (attachment.id)}
|
||||
{#if isImageUpload(attachment)}
|
||||
<button
|
||||
type="button"
|
||||
class="image-attachment"
|
||||
aria-label={`Open image ${attachment.filename}`}
|
||||
onclick={() => openImageViewer(uploadURL(attachment), attachment.filename)}
|
||||
>
|
||||
<img src={uploadURL(attachment)} alt={attachment.filename} loading="lazy" />
|
||||
<span>{attachment.filename}</span>
|
||||
</button>
|
||||
{:else if isVideoUpload(attachment)}
|
||||
<div class="video-attachment">
|
||||
<video controls preload="metadata" aria-label={attachment.filename}>
|
||||
<source src={uploadURL(attachment)} type={attachment.content_type} />
|
||||
</video>
|
||||
<a href={uploadURL(attachment)} target="_blank" rel="noreferrer">{attachment.filename}</a>
|
||||
</div>
|
||||
{:else}
|
||||
<a class="file-attachment" href={uploadURL(attachment)} target="_blank" rel="noreferrer">
|
||||
<span class="file-icon" aria-hidden="true">↧</span>
|
||||
<span>
|
||||
<strong>{attachment.filename}</strong>
|
||||
<small>{formatBytes(attachment.byte_size)}</small>
|
||||
</span>
|
||||
</a>
|
||||
{/if}
|
||||
<MediaAttachment
|
||||
upload={attachment}
|
||||
url={uploadURL(attachment)}
|
||||
onOpenImage={openImageViewer}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
137
apps/web/src/components/MediaAttachment.svelte
Normal file
137
apps/web/src/components/MediaAttachment.svelte
Normal file
@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import type { Upload } from "../lib/types";
|
||||
|
||||
type Props = {
|
||||
upload: Upload;
|
||||
url: string;
|
||||
onOpenImage?: (url: string, title: string) => void;
|
||||
};
|
||||
|
||||
let { upload, url, onOpenImage = () => {} }: Props = $props();
|
||||
|
||||
let videoEl: HTMLVideoElement | null = $state(null);
|
||||
let started = $state(false);
|
||||
let durationLabel = $state("");
|
||||
|
||||
let isImage = $derived(upload.content_type?.startsWith("image/") ?? false);
|
||||
let isVideo = $derived(upload.content_type?.startsWith("video/") ?? false);
|
||||
|
||||
function handlePlay() {
|
||||
started = true;
|
||||
}
|
||||
|
||||
function handleLoadedMetadata() {
|
||||
if (!videoEl || !isFinite(videoEl.duration)) return;
|
||||
const total = Math.floor(videoEl.duration);
|
||||
const m = Math.floor(total / 60);
|
||||
const s = total % 60;
|
||||
durationLabel = `${m}:${s.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function startPlayback() {
|
||||
if (!videoEl) return;
|
||||
started = true;
|
||||
void videoEl.play();
|
||||
}
|
||||
|
||||
function formatBytes(size: number) {
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
|
||||
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isImage}
|
||||
<div class="media-tile media-tile--image">
|
||||
<button
|
||||
type="button"
|
||||
class="media-tile__open"
|
||||
aria-label={`Open image ${upload.filename}`}
|
||||
onclick={() => onOpenImage(url, upload.filename)}
|
||||
>
|
||||
<img src={url} alt={upload.filename} loading="lazy" />
|
||||
</button>
|
||||
<div class="media-tile__caption">
|
||||
<span class="media-tile__name">{upload.filename}</span>
|
||||
<a
|
||||
class="media-tile__chip"
|
||||
href={url}
|
||||
download={upload.filename}
|
||||
aria-label={`Download ${upload.filename}`}
|
||||
onclick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 4v12m0 0 4-4m-4 4-4-4M5 20h14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else if isVideo}
|
||||
<div class="media-tile media-tile--video" class:is-started={started}>
|
||||
<video
|
||||
bind:this={videoEl}
|
||||
preload="metadata"
|
||||
playsinline
|
||||
controls={started}
|
||||
controlslist="nodownload"
|
||||
aria-label={upload.filename}
|
||||
onplay={handlePlay}
|
||||
onloadedmetadata={handleLoadedMetadata}
|
||||
>
|
||||
<source src={url} type={upload.content_type} />
|
||||
</video>
|
||||
{#if !started}
|
||||
<button
|
||||
type="button"
|
||||
class="media-tile__play"
|
||||
aria-label={`Play ${upload.filename}`}
|
||||
onclick={startPlayback}
|
||||
>
|
||||
<span class="media-tile__play-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="26" height="26">
|
||||
<path fill="currentColor" d="M8 5.5v13l11-6.5z" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
{#if durationLabel}
|
||||
<span class="media-tile__duration" aria-hidden="true">{durationLabel}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="media-tile__caption">
|
||||
<span class="media-tile__name">{upload.filename}</span>
|
||||
<a
|
||||
class="media-tile__chip"
|
||||
href={url}
|
||||
download={upload.filename}
|
||||
aria-label={`Download ${upload.filename}`}
|
||||
onclick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 4v12m0 0 4-4m-4 4-4-4M5 20h14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<a class="file-attachment" href={url} target="_blank" rel="noreferrer">
|
||||
<span class="file-icon" aria-hidden="true">↧</span>
|
||||
<span>
|
||||
<strong>{upload.filename}</strong>
|
||||
<small>{formatBytes(upload.byte_size)}</small>
|
||||
</span>
|
||||
</a>
|
||||
{/if}
|
||||
@ -1353,92 +1353,249 @@ button.ghost {
|
||||
|
||||
.attachment-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 280px));
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.attachment-grid.compact {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.image-attachment,
|
||||
.video-attachment,
|
||||
.file-attachment {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.image-attachment,
|
||||
.video-attachment {
|
||||
.media-tile {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--panel);
|
||||
box-shadow: 0 12px 30px -24px rgba(0, 0, 0, 0.8);
|
||||
background: #0a0c12;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
text-align: left;
|
||||
cursor: zoom-in;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 120ms ease,
|
||||
transform 120ms ease,
|
||||
box-shadow 120ms ease;
|
||||
border-color 140ms ease,
|
||||
transform 140ms ease,
|
||||
box-shadow 140ms ease;
|
||||
}
|
||||
|
||||
.image-attachment:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 42%, var(--line));
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 16px 36px -26px rgba(0, 0, 0, 0.9);
|
||||
.media-tile:hover {
|
||||
border-color: var(--line-strong);
|
||||
box-shadow: 0 18px 40px -28px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.video-attachment {
|
||||
.media-tile:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--line-strong);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent);
|
||||
}
|
||||
|
||||
.media-tile--image {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.media-tile__open {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.media-tile__open:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.media-tile__open:focus-visible + .media-tile__caption .media-tile__name {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.media-tile--video {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.image-attachment img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 320px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-attachment video {
|
||||
.media-tile img,
|
||||
.media-tile video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 360px;
|
||||
object-fit: contain;
|
||||
background: #05070d;
|
||||
}
|
||||
|
||||
.image-attachment span {
|
||||
.media-tile--image img {
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.media-tile__caption {
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 22px 10px 8px;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(0, 0, 0, 0.78) 0%,
|
||||
rgba(0, 0, 0, 0.5) 40%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
opacity: 0;
|
||||
pointer-events: auto;
|
||||
transition: opacity 140ms ease;
|
||||
}
|
||||
|
||||
.media-tile:hover .media-tile__caption,
|
||||
.media-tile:focus-within .media-tile__caption {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.media-tile--video.is-started .media-tile__caption {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.media-tile__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.media-tile__chip {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex: none;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
backdrop-filter: blur(10px);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background 120ms ease,
|
||||
border-color 120ms ease,
|
||||
transform 120ms ease;
|
||||
}
|
||||
|
||||
.media-tile__chip:hover {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-color: rgba(255, 255, 255, 0.32);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.media-tile__play {
|
||||
position: absolute;
|
||||
inset: 0 0 48px 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.media-tile__play::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(0, 0, 0, 0.18) 0%,
|
||||
rgba(0, 0, 0, 0.05) 45%,
|
||||
rgba(0, 0, 0, 0) 70%
|
||||
);
|
||||
opacity: 0.7;
|
||||
transition: opacity 160ms ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.media-tile__play:hover::before {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.media-tile__play-icon {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: white;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
box-shadow: 0 6px 20px -8px rgba(0, 0, 0, 0.55);
|
||||
padding-left: 3px; /* optical centering of the play glyph */
|
||||
transition:
|
||||
transform 200ms cubic-bezier(0.2, 0.8, 0.2, 1),
|
||||
background 180ms ease,
|
||||
box-shadow 200ms ease;
|
||||
}
|
||||
|
||||
.media-tile__play-icon svg {
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
|
||||
.media-tile__play:hover .media-tile__play-icon {
|
||||
transform: scale(1.06);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
box-shadow: 0 10px 28px -10px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.media-tile__play:active .media-tile__play-icon {
|
||||
transform: scale(0.97);
|
||||
transition-duration: 80ms;
|
||||
}
|
||||
|
||||
.media-tile__play:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.media-tile__play:focus-visible .media-tile__play-icon {
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(255, 255, 255, 0.6),
|
||||
0 10px 28px -10px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.media-tile__duration {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
bottom: 8px;
|
||||
max-width: calc(100% - 16px);
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.58);
|
||||
z-index: 2;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.video-attachment a {
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
background: var(--panel-2);
|
||||
}
|
||||
|
||||
.video-attachment a:hover {
|
||||
color: var(--text-strong);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-attachment {
|
||||
@ -2170,6 +2327,10 @@ button.ghost {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.modal-backdrop:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.profile-modal {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@ -2341,7 +2502,6 @@ button.ghost {
|
||||
width: min(1120px, 100%);
|
||||
max-height: min(86vh, 900px);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 14px;
|
||||
background: #07090f;
|
||||
color: white;
|
||||
|
||||
@ -135,7 +135,9 @@ test("sends messages, searches, uploads, opens a thread, and creates a DM", asyn
|
||||
await expect(page.getByText("pixel.png")).toBeVisible();
|
||||
await page.getByLabel("Message body").fill("inline image upload");
|
||||
await page.getByRole("button", { name: "Send" }).click();
|
||||
await expect(page.locator(".image-attachment").filter({ hasText: "pixel.png" })).toBeVisible();
|
||||
const imageAttachment = page.locator(".media-tile--image").filter({ hasText: "pixel.png" });
|
||||
await expect(imageAttachment).toBeVisible();
|
||||
await expect(imageAttachment.getByRole("link", { name: "Download pixel.png" })).toBeAttached();
|
||||
await page.getByRole("button", { name: "Open image pixel.png" }).click();
|
||||
await expect(
|
||||
page.getByLabel("Image viewer").getByRole("img", { name: "pixel.png" }),
|
||||
@ -153,7 +155,32 @@ test("sends messages, searches, uploads, opens a thread, and creates a DM", asyn
|
||||
await expect(page.getByText("clip.mp4")).toBeVisible();
|
||||
await page.getByLabel("Message body").fill("inline video upload");
|
||||
await page.getByRole("button", { name: "Send" }).click();
|
||||
await expect(page.locator('.video-attachment video[aria-label="clip.mp4"]')).toBeVisible();
|
||||
const videoAttachment = page.locator(".media-tile--video").filter({ hasText: "clip.mp4" });
|
||||
const inlineVideo = videoAttachment.locator('video[aria-label="clip.mp4"]');
|
||||
const videoDownload = videoAttachment.getByRole("link", { name: "Download clip.mp4" });
|
||||
await expect(inlineVideo).toBeVisible();
|
||||
await page.evaluate(() => {
|
||||
(window as unknown as { __videoDownloadClicked: boolean }).__videoDownloadClicked = false;
|
||||
});
|
||||
await videoDownload.evaluate((node) => {
|
||||
node.addEventListener(
|
||||
"click",
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
(window as unknown as { __videoDownloadClicked: boolean }).__videoDownloadClicked = true;
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
await videoDownload.click();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
() => (window as unknown as { __videoDownloadClicked: boolean }).__videoDownloadClicked,
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
await expect(inlineVideo).not.toHaveAttribute("controls", "");
|
||||
|
||||
await page.getByRole("button", { name: "GIF picker" }).click();
|
||||
await page.getByLabel("Search GIFs").fill("ship");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user