Attacment Modal: Fix multiline height and better support for drag-n-drop (#1113)

This commit is contained in:
Alexander Zinchuk 2021-05-24 13:10:03 +03:00
parent 800a23f731
commit 01933bfcb1
6 changed files with 187 additions and 55 deletions

View File

@ -74,4 +74,58 @@
z-index: 0;
}
}
.drop-target {
position: relative;
&::before,
&::after {
content: '';
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border-radius: var(--border-radius-default);
pointer-events: none;
opacity: 0;
transition: 250ms opacity;
z-index: 1;
}
&::before {
background-image: var(--drag-target-border-hovered);
background-color: var(--color-background);
}
&::after {
content: attr(data-attach-description);
display: flex;
justify-content: center;
align-items: center;
color: var(--color-primary);
}
}
&.hovered {
.drop-target::before {
opacity: .95;
}
.drop-target::after {
opacity: 1;
}
.attachment-caption-wrapper,
.document-wrapper,
.media-wrapper {
pointer-events: none;
}
.document-wrapper,
.media-wrapper {
border-radius: var(--border-radius-default);
}
}
}

View File

@ -1,9 +1,9 @@
import React, {
FC, memo, useCallback, useEffect,
FC, memo, useCallback, useEffect, useRef,
} from '../../../lib/teact/teact';
import { ApiAttachment, ApiChatMember, ApiUser } from '../../../api/types';
import { EDITABLE_INPUT_MODAL_ID } from '../../../config';
import { CONTENT_TYPES_FOR_QUICK_UPLOAD, EDITABLE_INPUT_MODAL_ID } from '../../../config';
import { getFileExtension } from '../../common/helpers/documentInfo';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
@ -11,6 +11,7 @@ import usePrevious from '../../../hooks/usePrevious';
import useMentionTooltip from './hooks/useMentionTooltip';
import useEmojiTooltip from './hooks/useEmojiTooltip';
import useLang from '../../../hooks/useLang';
import useFlag from '../../../hooks/useFlag';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
@ -33,9 +34,12 @@ export type OwnProps = {
addRecentEmoji: AnyToVoidFunction;
onCaptionUpdate: (html: string) => void;
onSend: () => void;
onFileAppend: (files: File[], isQuick: boolean) => void;
onClear: () => void;
};
const DROP_LEAVE_TIMEOUT_MS = 150;
const AttachmentModal: FC<OwnProps> = ({
attachments,
caption,
@ -47,11 +51,17 @@ const AttachmentModal: FC<OwnProps> = ({
onCaptionUpdate,
addRecentEmoji,
onSend,
onFileAppend,
onClear,
}) => {
// eslint-disable-next-line no-null/no-null
const hideTimeoutRef = useRef<number>(null);
const prevAttachments = usePrevious(attachments);
const renderingAttachments = attachments.length ? attachments : prevAttachments;
const isOpen = Boolean(attachments.length);
const [isHovered, markHovered, unmarkHovered] = useFlag();
const isQuick = renderingAttachments && renderingAttachments.every((a) => a.quick);
const lang = useLang();
const {
isMentionTooltipOpen, mentionFilter,
@ -84,7 +94,51 @@ const AttachmentModal: FC<OwnProps> = ({
}
}, [isOpen, onSend]);
const lang = useLang();
const handleDragLeave = (e: React.DragEvent<HTMLElement>) => {
const { relatedTarget: toTarget, target: fromTarget } = e;
// Esc button pressed during drag event
if ((fromTarget as HTMLDivElement).matches('.drop-target') && !toTarget) {
hideTimeoutRef.current = window.setTimeout(unmarkHovered, DROP_LEAVE_TIMEOUT_MS);
}
// Prevent DragLeave event from firing when the pointer moves inside the AttachmentModal drop target
if (fromTarget && (fromTarget as HTMLElement).closest('.AttachmentModal.hovered')) {
return;
}
if (toTarget) {
e.stopPropagation();
}
unmarkHovered();
};
const handleFilesDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
unmarkHovered();
const { dataTransfer: { files } } = e;
if (files && files.length) {
const newFiles = isQuick
? Array.from(files).filter((file) => {
return file.type && CONTENT_TYPES_FOR_QUICK_UPLOAD.includes(file.type);
})
: Array.from(files);
onFileAppend(newFiles, false);
}
}, [isQuick, onFileAppend, unmarkHovered]);
function handleDragOver(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
e.preventDefault();
e.stopPropagation();
if (hideTimeoutRef.current) {
window.clearTimeout(hideTimeoutRef.current);
}
}
if (!renderingAttachments) {
return undefined;
@ -102,8 +156,6 @@ const AttachmentModal: FC<OwnProps> = ({
title = renderingAttachments.length === 1 ? 'Send File' : `Send ${renderingAttachments.length} Files`;
}
const isQuick = renderingAttachments.every((a) => a.quick);
function renderHeader() {
if (!renderingAttachments) {
return undefined;
@ -128,54 +180,68 @@ const AttachmentModal: FC<OwnProps> = ({
}
return (
<Modal isOpen={isOpen} onClose={onClear} header={renderHeader()} className="AttachmentModal">
{isQuick ? (
<div className="media-wrapper custom-scroll">
{renderingAttachments.map((attachment) => (
attachment.mimeType.startsWith('image/')
? <img src={attachment.blobUrl} alt="" />
: <video src={attachment.blobUrl} autoPlay muted loop />
))}
</div>
) : (
<div className="document-wrapper custom-scroll">
{renderingAttachments.map((attachment) => (
<File
name={attachment.filename}
extension={getFileExtension(attachment.filename, attachment.mimeType)}
previewData={attachment.previewBlobUrl}
size={attachment.size}
smaller
/>
))}
</div>
)}
<Modal
isOpen={isOpen}
onClose={onClear}
header={renderHeader()}
className={`AttachmentModal ${isHovered ? 'hovered' : ''}`}
>
<div
className="drop-target"
onDragEnter={markHovered}
onDrop={handleFilesDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
data-attach-description={lang('Preview.Dragging.AddItems', 10)}
>
{isQuick ? (
<div className="media-wrapper custom-scroll">
{renderingAttachments.map((attachment) => (
attachment.mimeType.startsWith('image/')
? <img src={attachment.blobUrl} alt="" />
: <video src={attachment.blobUrl} autoPlay muted loop />
))}
</div>
) : (
<div className="document-wrapper custom-scroll">
{renderingAttachments.map((attachment) => (
<File
name={attachment.filename}
extension={getFileExtension(attachment.filename, attachment.mimeType)}
previewData={attachment.previewBlobUrl}
size={attachment.size}
smaller
/>
))}
</div>
)}
<div className="attachment-caption-wrapper">
<MentionTooltip
isOpen={isMentionTooltipOpen}
onClose={closeMentionTooltip}
filter={mentionFilter}
onInsertUserName={insertMention}
filteredChatMembers={mentionFilteredMembers}
usersById={usersById}
/>
<EmojiTooltip
isOpen={isEmojiTooltipOpen}
emojis={filteredEmojis}
onClose={closeEmojiTooltip}
onEmojiSelect={insertEmoji}
addRecentEmoji={addRecentEmoji}
/>
<MessageInput
id="caption-input-text"
html={caption}
editableInputId={EDITABLE_INPUT_MODAL_ID}
placeholder={lang('Caption')}
onUpdate={onCaptionUpdate}
onSend={onSend}
shouldSetFocus={isOpen}
/>
<div className="attachment-caption-wrapper">
<MentionTooltip
isOpen={isMentionTooltipOpen}
onClose={closeMentionTooltip}
filter={mentionFilter}
onInsertUserName={insertMention}
filteredChatMembers={mentionFilteredMembers}
usersById={usersById}
/>
<EmojiTooltip
isOpen={isEmojiTooltipOpen}
emojis={filteredEmojis}
onClose={closeEmojiTooltip}
onEmojiSelect={insertEmoji}
addRecentEmoji={addRecentEmoji}
/>
<MessageInput
id="caption-input-text"
html={caption}
editableInputId={EDITABLE_INPUT_MODAL_ID}
placeholder={lang('Caption')}
onUpdate={onCaptionUpdate}
onSend={onSend}
shouldSetFocus={isOpen}
/>
</div>
</div>
</Modal>
);

View File

@ -384,6 +384,13 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
setAttachments(await Promise.all(files.map((file) => buildAttachment(file.name, file, isQuick))));
}, []);
const handleAppendFiles = useCallback(async (files: File[], isQuick: boolean) => {
setAttachments([
...attachments,
...await Promise.all(files.map((file) => buildAttachment(file.name, file, isQuick))),
]);
}, [attachments]);
const handleClearAttachment = useCallback(() => {
setAttachments([]);
}, []);
@ -683,6 +690,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
onCaptionUpdate={setHtml}
addRecentEmoji={addRecentEmoji}
onSend={shouldSchedule ? openCalendar : handleSend}
onFileAppend={handleAppendFiles}
onClear={handleClearAttachment}
/>
<PollModal

View File

@ -17,7 +17,7 @@
}
&.hovered .target-content {
color: #63A2E3;
color: var(--color-primary);
background-image: var(--drag-target-border-hovered);
}

View File

@ -97,6 +97,10 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
const [textFormatterAnchorPosition, setTextFormatterAnchorPosition] = useState<IAnchorPosition>();
const [selectedRange, setSelectedRange] = useState<Range>();
useEffect(() => {
updateInputHeight(false);
}, []);
useLayoutEffectWithPrevDeps(([prevHtml]) => {
if (html !== inputRef.current!.innerHTML) {
inputRef.current!.innerHTML = html;

View File

@ -28,7 +28,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
export const LANG_CACHE_NAME = 'tt-lang-packs-v2';
export const LANG_CACHE_NAME = 'tt-lang-packs-v3';
export const ASSET_CACHE_NAME = 'tt-assets';
export const API_UPDATE_THROTTLE = 300;
@ -124,7 +124,7 @@ export const MAX_MEDIA_FILES_FOR_ALBUM = 10;
export const MAX_ACTIVE_PINNED_CHATS = 5;
export const SCHEDULED_WHEN_ONLINE = 0x7FFFFFFE;
export const DEFAULT_LANG_PACK = 'android';
export const LANG_PACKS = ['android', 'ios', 'tdesktop'];
export const LANG_PACKS = ['android', 'ios', 'tdesktop', 'macos'];
export const TIPS_USERNAME = 'TelegramTips';
export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time';
export const DARK_THEME_BG_COLOR = '#0F0F0F';