Modals for media attachments in composer

Dedicated editor experience per media attachment
pull/42/head
Lim Chee Aun 2023-01-06 01:51:39 +08:00
rodzic 228c74655a
commit 3ca696dd3d
3 zmienionych plików z 161 dodań i 29 usunięć

Wyświetl plik

@ -655,6 +655,9 @@ button.carousel-dot[disabled].active {
padding-right: max(16px, env(safe-area-inset-right));
user-select: none;
}
.sheet header :is(h1, h2, h3) {
margin: 0;
}
.sheet main {
overflow: auto;
overflow-x: hidden;

Wyświetl plik

@ -255,16 +255,34 @@
align-items: stretch;
}
#compose-container .media-preview {
flex-shrink: 1;
flex-shrink: 0;
border: 1px solid var(--outline-color);
border-radius: 4px;
overflow: hidden;
width: 80px;
height: 80px;
/* checkerboard background */
background-image: linear-gradient(
45deg,
var(--img-bg-color) 25%,
transparent 25%
),
linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%),
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
background-size: 10px 10px;
background-position: 0 0, 0 5px, 5px -5px, -5px 0px;
}
#compose-container .media-preview > * {
min-width: 80px;
width: 80px !important;
width: 80px;
height: 80px;
object-fit: contain;
background-color: var(--img-bg-color);
border-radius: 8px;
border: 1px solid var(--outline-color);
vertical-align: middle;
pointer-events: none;
}
#compose-container .media-preview:hover {
box-shadow: 0 0 0 2px var(--link-light-color);
cursor: pointer;
}
#compose-container .media-attachment textarea {
height: 80px;
@ -389,3 +407,39 @@
display: none;
}
}
#media-sheet main {
padding-top: 8px;
display: flex;
flex-direction: column;
flex: 1;
}
#media-sheet textarea {
width: 100%;
height: 10em;
margin-top: 8px;
}
#media-sheet .media-preview {
border: 2px solid var(--outline-color);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 16px var(--img-bg-color);
/* checkerboard background */
background-image: linear-gradient(
45deg,
var(--img-bg-color) 25%,
transparent 25%
),
linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%),
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
#media-sheet .media-preview > * {
width: 100%;
height: 100%;
max-height: 50vh;
object-fit: contain;
vertical-align: middle;
}

Wyświetl plik

@ -13,11 +13,13 @@ import emojifyText from '../utils/emojify-text';
import openCompose from '../utils/open-compose';
import states from '../utils/states';
import store from '../utils/store';
import useDebouncedCallback from '../utils/useDebouncedCallback';
import visibilityIconsMap from '../utils/visibility-icons-map';
import Avatar from './avatar';
import Icon from './icon';
import Loader from './loader';
import Modal from './modal';
import Status from './status';
const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
@ -1090,26 +1092,41 @@ function MediaAttachment({
onDescriptionChange = () => {},
onRemove = () => {},
}) {
const { url, type, id, description } = attachment;
const { url, type, id } = attachment;
console.log({ attachment });
const [description, setDescription] = useState(attachment.description);
const suffixType = type.split('/')[0];
return (
<div class="media-attachment">
<div class="media-preview">
{suffixType === 'image' ? (
<img src={url} alt="" />
) : suffixType === 'video' || suffixType === 'gifv' ? (
<video src={url} playsinline muted />
) : suffixType === 'audio' ? (
<audio src={url} controls />
) : null}
</div>
const debouncedOnDescriptionChange = useDebouncedCallback(
onDescriptionChange,
500,
);
const [showModal, setShowModal] = useState(false);
const textareaRef = useRef(null);
useEffect(() => {
let timer;
if (showModal && textareaRef.current) {
timer = setTimeout(() => {
textareaRef.current.focus();
}, 100);
}
return () => {
clearTimeout(timer);
};
}, [showModal]);
const descTextarea = (
<>
{!!id ? (
<div class="media-desc">
<span class="tag">Uploaded</span>
<p title={description}>{description || <i>No description</i>}</p>
<p title={description}>
{attachment.description || <i>No description</i>}
</p>
</div>
) : (
<textarea
ref={textareaRef}
value={description || ''}
placeholder={
{
@ -1128,21 +1145,79 @@ function MediaAttachment({
// TODO: Un-hard-code this maxlength, ref: https://github.com/mastodon/mastodon/blob/b59fb28e90bc21d6fd1a6bafd13cfbd81ab5be54/app/models/media_attachment.rb#L39
onInput={(e) => {
const { value } = e.target;
onDescriptionChange(value);
setDescription(value);
debouncedOnDescriptionChange(value);
}}
></textarea>
)}
<div class="media-aside">
<button
type="button"
class="plain close-button"
disabled={disabled}
onClick={onRemove}
</>
);
return (
<>
<div class="media-attachment">
<div
class="media-preview"
onClick={() => {
setShowModal(true);
}}
>
<Icon icon="x" />
</button>
{suffixType === 'image' ? (
<img src={url} alt="" />
) : suffixType === 'video' || suffixType === 'gifv' ? (
<video src={url} playsinline muted />
) : suffixType === 'audio' ? (
<audio src={url} controls />
) : null}
</div>
{descTextarea}
<div class="media-aside">
<button
type="button"
class="plain close-button"
disabled={disabled}
onClick={onRemove}
>
<Icon icon="x" />
</button>
</div>
</div>
</div>
{showModal && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowModal(false);
}
}}
>
<div id="media-sheet" class="sheet">
<header>
<h2>
{
{
image: 'Edit image description',
video: 'Edit video description',
audio: 'Edit audio description',
}[suffixType]
}
</h2>
</header>
<main tabIndex="-1">
<div class="media-preview">
{suffixType === 'image' ? (
<img src={url} alt="" />
) : suffixType === 'video' || suffixType === 'gifv' ? (
<video src={url} playsinline controls />
) : suffixType === 'audio' ? (
<audio src={url} controls />
) : null}
</div>
{descTextarea}
</main>
</div>
</Modal>
)}
</>
);
}