Add support for displaying link previews for Admin UI (#35958)
This commit is contained in:
@@ -57,6 +57,20 @@ module StatusesHelper
|
||||
components.compact_blank.join("\n\n")
|
||||
end
|
||||
|
||||
# This logic should be kept in sync with https://github.com/mastodon/mastodon/blob/425311e1d95c8a64ddac6c724fca247b8b893a82/app/javascript/mastodon/features/status/components/card.jsx#L160
|
||||
def preview_card_aspect_ratio_classname(preview_card)
|
||||
interactive = preview_card.type == 'video'
|
||||
large_image = (preview_card.image.present? && preview_card.width > preview_card.height) || interactive
|
||||
|
||||
if large_image && interactive
|
||||
'status-card__image--video'
|
||||
elsif large_image
|
||||
'status-card__image--large'
|
||||
else
|
||||
'status-card__image--normal'
|
||||
end
|
||||
end
|
||||
|
||||
def visibility_icon(status)
|
||||
VISIBLITY_ICONS[status.visibility.to_sym]
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import Rails from '@rails/ujs';
|
||||
import { decode, ValidationError } from 'blurhash';
|
||||
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
@@ -362,6 +363,46 @@ ready(() => {
|
||||
document.querySelectorAll('[data-admin-component]').forEach((element) => {
|
||||
void mountReactComponent(element);
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLCanvasElement>('canvas[data-blurhash]')
|
||||
.forEach((canvas) => {
|
||||
const blurhash = canvas.dataset.blurhash;
|
||||
if (blurhash) {
|
||||
try {
|
||||
// decode returns a Uint8ClampedArray<ArrayBufferLike> not Uint8ClampedArray<ArrayBuffer>
|
||||
const pixels = decode(
|
||||
blurhash,
|
||||
32,
|
||||
32,
|
||||
) as Uint8ClampedArray<ArrayBuffer>;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, 32, 32);
|
||||
|
||||
ctx?.putImageData(imageData, 0, 0);
|
||||
} catch (err) {
|
||||
if (err instanceof ValidationError) {
|
||||
// ignore blurhash validation errors
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLDivElement>('.preview-card')
|
||||
.forEach((previewCard) => {
|
||||
const spoilerButton = previewCard.querySelector('.spoiler-button');
|
||||
if (!spoilerButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
spoilerButton.addEventListener('click', () => {
|
||||
previewCard.classList.toggle('preview-card--image-visible');
|
||||
});
|
||||
});
|
||||
}).catch((reason: unknown) => {
|
||||
throw reason;
|
||||
});
|
||||
|
||||
@@ -1969,6 +1969,61 @@ a.sparkline {
|
||||
display: list-item;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
position: relative;
|
||||
max-width: 566px;
|
||||
|
||||
.status-card__image {
|
||||
&--video {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
&--large {
|
||||
aspect-ratio: 1.91 / 1;
|
||||
}
|
||||
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.spoiler-button__overlay__label {
|
||||
outline: 1px solid var(--media-outline-color);
|
||||
}
|
||||
|
||||
.hide-button {
|
||||
// Toggled to appear when the preview-card is unblurred:
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
color: $white;
|
||||
border: 0;
|
||||
outline: 1px solid var(--media-outline-color);
|
||||
background-color: color.change($black, $alpha: 0.45);
|
||||
backdrop-filter: $backdrop-blur-filter;
|
||||
padding: 3px 12px;
|
||||
border-radius: 99px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: color.change($black, $alpha: 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
&.preview-card--image-visible {
|
||||
.hide-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spoiler-button__overlay,
|
||||
.status-card__image-preview {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin {
|
||||
|
||||
@@ -52,6 +52,10 @@ class StatusEdit < ApplicationRecord
|
||||
underlying_quote
|
||||
end
|
||||
|
||||
def with_preview_card?
|
||||
false
|
||||
end
|
||||
|
||||
def with_media?
|
||||
ordered_media_attachments.any?
|
||||
end
|
||||
|
||||
30
app/views/admin/shared/_preview_card.html.haml
Normal file
30
app/views/admin/shared/_preview_card.html.haml
Normal file
@@ -0,0 +1,30 @@
|
||||
/# locals: (preview_card:)
|
||||
|
||||
.preview-card
|
||||
.status-card.expanded
|
||||
.status-card__image{ class: preview_card_aspect_ratio_classname(preview_card) }
|
||||
.spoiler-button
|
||||
%button.hide-button{ type: 'button' }= t('link_preview.potentially_sensitive_content.hide_button')
|
||||
%button.spoiler-button__overlay{ type: 'button' }
|
||||
%span.spoiler-button__overlay__label
|
||||
%span= t('link_preview.potentially_sensitive_content.label')
|
||||
%span.spoiler-button__overlay__action
|
||||
%span= t('link_preview.potentially_sensitive_content.action')
|
||||
%canvas.status-card__image-preview{ 'data-blurhash': preview_card.blurhash, width: 32, height: 32 }
|
||||
= image_tag preview_card.image.url, alt: '', class: 'status-card__image-image'
|
||||
= link_to preview_card.url, target: '_blank', rel: 'noopener', data: { confirm: t('link_preview.potentially_sensitive_content.confirm_visit') } do
|
||||
.status-card__content{ dir: 'auto' }
|
||||
%span.status-card__host
|
||||
%span{ lang: preview_card.language }
|
||||
= preview_card.provider_name
|
||||
- if preview_card.published_at
|
||||
·
|
||||
%time.relative-formatted{ datetime: preview_card.published_at.iso8601, title: l(preview_card.published_at) }= l(preview_card.published_at)
|
||||
%strong.status-card__title{ title: preview_card.title, lang: preview_card.language }
|
||||
= preview_card.title
|
||||
- if preview_card.author_name.present?
|
||||
%span.status-card__author
|
||||
= t('link_preview.author_html', name: content_tag(:strong, preview_card.author_name))
|
||||
- else
|
||||
%span.status-card__description{ lang: preview_card.language }
|
||||
= preview_card.description
|
||||
@@ -13,6 +13,9 @@
|
||||
%button.button.button-secondary{ disabled: true }
|
||||
= t('polls.vote')
|
||||
|
||||
- if status.with_preview_card?
|
||||
= render partial: 'admin/shared/preview_card', locals: { preview_card: status.preview_card }
|
||||
|
||||
- if status.with_media?
|
||||
- if status.ordered_media_attachments.first.video?
|
||||
= render_video_component(status, visible: false)
|
||||
|
||||
@@ -1591,6 +1591,13 @@ en:
|
||||
expires_at: Expires
|
||||
uses: Uses
|
||||
title: Invite people
|
||||
link_preview:
|
||||
author_html: By %{name}
|
||||
potentially_sensitive_content:
|
||||
action: Click to show
|
||||
confirm_visit: Are you sure you wish to open this link?
|
||||
hide_button: Hide
|
||||
label: Potentially sensitive content
|
||||
lists:
|
||||
errors:
|
||||
limit: You have reached the maximum number of lists
|
||||
|
||||
Reference in New Issue
Block a user