Adjustments to composer footer buttons

- Make it one-liner
- Make the add-action buttons scrollable
- Introduce 'Add' button that shows a menu of the actions to allow more actions in the future
This commit is contained in:
Lim Chee Aun 2024-11-27 22:22:09 +08:00
parent b6d1522480
commit 28bdd9a0fa
2 changed files with 335 additions and 115 deletions

View file

@ -20,11 +20,15 @@
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
padding: 16px; padding: 8px;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 100; z-index: 100;
white-space: nowrap; white-space: nowrap;
@media (min-width: 480px) {
padding: 16px;
}
} }
#compose-container .compose-top .account-block { #compose-container .compose-top .account-block {
text-align: start; text-align: start;
@ -110,10 +114,10 @@
} }
#compose-container form { #compose-container form {
--form-padding-inline: 8px; --form-spacing-inline: 4px;
--form-padding-block: 0; --form-spacing-block: 0;
/* border-radius: 16px; */ /* border-radius: 16px; */
padding: var(--form-padding-block) var(--form-padding-inline); padding: var(--form-spacing-block) var(--form-spacing-inline);
background-color: var(--bg-blur-color); background-color: var(--bg-blur-color);
/* background-image: linear-gradient(var(--bg-color) 85%, transparent); */ /* background-image: linear-gradient(var(--bg-color) 85%, transparent); */
position: relative; position: relative;
@ -121,6 +125,10 @@
--drop-shadow: 0 3px 6px -3px var(--drop-shadow-color); --drop-shadow: 0 3px 6px -3px var(--drop-shadow-color);
box-shadow: var(--drop-shadow); box-shadow: var(--drop-shadow);
@media (min-width: 480px) {
--form-spacing-inline: 8px;
}
@media (min-width: 40em) { @media (min-width: 40em) {
border-radius: 16px; border-radius: 16px;
} }
@ -153,8 +161,8 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 8px 0; padding: var(--form-spacing-inline) 0;
gap: 8px; gap: var(--form-spacing-inline);
} }
#compose-container .toolbar.wrap { #compose-container .toolbar.wrap {
flex-wrap: wrap; flex-wrap: wrap;
@ -181,6 +189,11 @@
white-space: nowrap; white-space: nowrap;
border: 2px solid transparent; border: 2px solid transparent;
vertical-align: middle; vertical-align: middle;
&.active {
filter: brightness(0.8);
background-color: var(--bg-color);
}
} }
#compose-container .toolbar-button > * { #compose-container .toolbar-button > * {
vertical-align: middle; vertical-align: middle;
@ -248,6 +261,38 @@
max-width: 100%; max-width: 100%;
} }
#compose-container .compose-footer {
.add-toolbar-button-group {
display: flex;
overflow: auto;
}
.add-sub-toolbar-button-group {
flex-grow: 1;
display: flex;
overflow: auto;
transition: 0.5s ease-in-out;
transition-property: opacity, width;
scrollbar-width: none;
padding-inline-end: 16px;
mask-image: linear-gradient(
var(--to-backward),
transparent 0,
black 16px,
black 100%
);
&::-webkit-scrollbar {
display: none;
}
&[hidden] {
opacity: 0;
pointer-events: none;
width: 0;
}
}
}
#compose-container text-expander { #compose-container text-expander {
position: relative; position: relative;
display: block; display: block;
@ -516,6 +561,37 @@
color: var(--red-color); color: var(--red-color);
} }
.compose-menu-add-media {
position: relative;
.compose-menu-add-media-field {
position: absolute;
inset: 0;
opacity: 0;
cursor: inherit;
}
}
.icon-gif {
display: inline-block !important;
min-width: 16px;
height: 16px;
font-size: 10px !important;
letter-spacing: -0.5px;
font-size-adjust: none;
overflow: hidden;
white-space: nowrap;
text-align: center;
line-height: 16px;
font-weight: bold;
text-rendering: optimizeSpeed;
&:after {
display: block;
content: 'GIF';
}
}
@media (display-mode: standalone) { @media (display-mode: standalone) {
/* No popping in standalone mode */ /* No popping in standalone mode */
#compose-container .pop-button { #compose-container .pop-button {
@ -525,8 +601,10 @@
#compose-container button[type='submit'] { #compose-container button[type='submit'] {
border-radius: 8px; border-radius: 8px;
@media (min-width: 480px) { @media (min-width: 480px) {
padding-inline: 24px; padding-inline: 24px;
font-size: 125%;
} }
} }
@ -820,8 +898,8 @@
.compose-field-container { .compose-field-container {
display: grid !important; display: grid !important;
@media (width < 30em) { @media (width < 480px) {
margin-inline: calc(-1 * var(--form-padding-inline)); margin-inline: calc(-1 * var(--form-spacing-inline));
width: 100vw !important; width: 100vw !important;
max-width: 100vw; max-width: 100vw;
@ -929,15 +1007,55 @@
} }
} }
@keyframes jump-scare {
from {
opacity: 0.5;
transform: scale(0.25) translateX(80px);
}
to {
opacity: 1;
transform: scale(1) translateX(0);
}
}
@keyframes jump-scare-rtl {
from {
opacity: 0.5;
transform: scale(0.25) translateX(-80px);
}
to {
opacity: 1;
transform: scale(1) translateX(0);
}
}
.add-button {
transform-origin: var(--forward) center;
background-color: var(--bg-blur-color) !important;
animation: jump-scare 0.2s ease-in-out both;
:dir(rtl) & {
animation-name: jump-scare-rtl;
}
.icon {
transition: transform 0.3s ease-in-out;
}
&.active {
.icon {
transform: rotate(135deg);
}
}
}
.gif-picker-button { .gif-picker-button {
span { /* span {
font-weight: bold; font-weight: bold;
font-size: 11.5px; font-size: 11.5px;
display: block; display: block;
} line-height: 1;
} */
&:is(:hover, :focus) { &:is(:hover, :focus) {
span { .icon {
animation: gif-shake 0.3s 3; animation: gif-shake 0.3s 3;
} }
} }

View file

@ -19,6 +19,7 @@ import stringLength from 'string-length';
// import { detectAll } from 'tinyld/light'; // import { detectAll } from 'tinyld/light';
import { uid } from 'uid/single'; import { uid } from 'uid/single';
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce'; import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
import useResizeObserver from 'use-resize-observer';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import poweredByGiphyURL from '../assets/powered-by-giphy.svg'; import poweredByGiphyURL from '../assets/powered-by-giphy.svg';
@ -201,6 +202,13 @@ const LF = mem((locale) => new Intl.ListFormat(locale || undefined));
const CUSTOM_EMOJIS_COUNT = 100; const CUSTOM_EMOJIS_COUNT = 100;
const ADD_LABELS = {
media: msg`Add media`,
customEmoji: msg`Add custom emoji`,
gif: msg`Add GIF`,
poll: msg`Add poll`,
};
function Compose({ function Compose({
onClose, onClose,
replyToStatus, replyToStatus,
@ -209,7 +217,7 @@ function Compose({
standalone, standalone,
hasOpener, hasOpener,
}) { }) {
const { i18n } = useLingui(); const { i18n, _ } = useLingui();
const rtf = RTF(i18n.locale); const rtf = RTF(i18n.locale);
const lf = LF(i18n.locale); const lf = LF(i18n.locale);
@ -732,6 +740,39 @@ function Compose({
states.composerState.minimized = true; states.composerState.minimized = true;
}; };
const gifPickerDisabled =
uiState === 'loading' ||
(maxMediaAttachments !== undefined &&
mediaAttachments.length >= maxMediaAttachments) ||
!!poll;
// If maxOptions is not defined or defined and is greater than 1, show poll button
const showPollButton = maxOptions == null || maxOptions > 1;
const pollButtonDisabled =
uiState === 'loading' || !!poll || !!mediaAttachments.length;
const onPollButtonClick = () => {
setPoll({
options: ['', ''],
expiresIn: 24 * 60 * 60, // 1 day
multiple: false,
});
};
const addSubToolbarRef = useRef();
const [showAddButton, setShowAddButton] = useState(false);
useResizeObserver({
ref: addSubToolbarRef,
box: 'border-box',
onResize: ({ width }) => {
// If scrollable, it's truncated
const { scrollWidth } = addSubToolbarRef.current;
const truncated = scrollWidth > width;
const overTruncated = width < 84; // roughly two buttons width
setShowAddButton(overTruncated || truncated);
addSubToolbarRef.current.hidden = overTruncated;
},
});
return ( return (
<div id="compose-container-outer"> <div id="compose-container-outer">
<div id="compose-container" class={standalone ? 'standalone' : ''}> <div id="compose-container" class={standalone ? 'standalone' : ''}>
@ -1318,86 +1359,87 @@ function Compose({
}} }}
/> />
)} )}
<div <div class="toolbar compose-footer">
class="toolbar wrap" <span class="add-toolbar-button-group spacer">
style={{ {showAddButton && (
justifyContent: 'flex-end', <Menu2
portal={{
target: document.body,
}} }}
containerProps={{
style: {
zIndex: 1001,
},
}}
menuButton={({ open }) => (
<button
class={`toolbar-button add-button ${
open ? 'active' : ''
}`}
> >
<span> <Icon icon="plus" title={t`Add`} />
<label class="toolbar-button"> </button>
<input )}
type="file" >
accept={supportedMimeTypes?.join(',')} <MenuItem className="compose-menu-add-media">
multiple={ <label class="compose-menu-add-media-field">
maxMediaAttachments === undefined || <FilePickerInput
maxMediaAttachments - mediaAttachments >= 2 hidden
} supportedMimeTypes={supportedMimeTypes}
maxMediaAttachments={maxMediaAttachments}
mediaAttachments={mediaAttachments}
disabled={ disabled={
uiState === 'loading' || uiState === 'loading' ||
mediaAttachments.length >= maxMediaAttachments || mediaAttachments.length >= maxMediaAttachments ||
!!poll !!poll
} }
onChange={(e) => { setMediaAttachments={setMediaAttachments}
const files = e.target.files;
if (!files) return;
const mediaFiles = Array.from(files).map((file) => ({
file,
type: file.type,
size: file.size,
url: URL.createObjectURL(file),
id: null, // indicate uploaded state
description: null,
}));
console.log('MEDIA ATTACHMENTS', files, mediaFiles);
// Validate max media attachments
if (
mediaAttachments.length + mediaFiles.length >
maxMediaAttachments
) {
alert(
plural(maxMediaAttachments, {
one: 'You can only attach up to 1 file.',
other: 'You can only attach up to # files.',
}),
);
} else {
setMediaAttachments((attachments) => {
return attachments.concat(mediaFiles);
});
}
// Reset
e.target.value = '';
}}
/> />
<Icon icon="attachment" />
</label> </label>
{/* If maxOptions is not defined or defined and is greater than 1, show poll button */} <Icon icon="media" /> <span>{_(ADD_LABELS.media)}</span>
{maxOptions == null || </MenuItem>
(maxOptions > 1 && ( <MenuItem
<>
<button
type="button"
class="toolbar-button"
disabled={
uiState === 'loading' ||
!!poll ||
!!mediaAttachments.length
}
onClick={() => { onClick={() => {
setPoll({ setShowEmoji2Picker(true);
options: ['', ''],
expiresIn: 24 * 60 * 60, // 1 day
multiple: false,
});
}} }}
> >
<Icon icon="poll" alt={t`Add poll`} /> <Icon icon="emoji2" />{' '}
</button> <span>{_(ADD_LABELS.customEmoji)}</span>
</> </MenuItem>
))} {!!states.settings.composerGIFPicker && (
<MenuItem
disabled={gifPickerDisabled}
onClick={() => {
setShowGIFPicker(true);
}}
>
<span class="icon icon-gif" role="img" />
<span>{_(ADD_LABELS.gif)}</span>
</MenuItem>
)}
<MenuItem
disabled={pollButtonDisabled}
onClick={onPollButtonClick}
>
<Icon icon="poll" /> <span>{_(ADD_LABELS.poll)}</span>
</MenuItem>
</Menu2>
)}
<span class="add-sub-toolbar-button-group" ref={addSubToolbarRef}>
<label class="toolbar-button">
<FilePickerInput
supportedMimeTypes={supportedMimeTypes}
maxMediaAttachments={maxMediaAttachments}
mediaAttachments={mediaAttachments}
disabled={
uiState === 'loading' ||
mediaAttachments.length >= maxMediaAttachments ||
!!poll
}
setMediaAttachments={setMediaAttachments}
/>
<Icon icon="media" alt={_(ADD_LABELS.media)} />
</label>
{/* <button {/* <button
type="button" type="button"
class="toolbar-button" class="toolbar-button"
@ -1416,27 +1458,39 @@ function Compose({
setShowEmoji2Picker(true); setShowEmoji2Picker(true);
}} }}
> >
<Icon icon="emoji2" alt={t`Add custom emoji`} /> <Icon icon="emoji2" alt={_(ADD_LABELS.customEmoji)} />
</button> </button>
{!!states.settings.composerGIFPicker && ( {!!states.settings.composerGIFPicker && (
<button <button
type="button" type="button"
class="toolbar-button gif-picker-button" class="toolbar-button gif-picker-button"
disabled={ disabled={gifPickerDisabled}
uiState === 'loading' ||
(maxMediaAttachments !== undefined &&
mediaAttachments.length >= maxMediaAttachments) ||
!!poll
}
onClick={() => { onClick={() => {
setShowGIFPicker(true); setShowGIFPicker(true);
}} }}
> >
<span>GIF</span> <span
class="icon icon-gif"
aria-label={_(ADD_LABELS.gif)}
/>
</button> </button>
)} )}
{}
{showPollButton && (
<>
<button
type="button"
class="toolbar-button"
disabled={pollButtonDisabled}
onClick={onPollButtonClick}
>
<Icon icon="poll" alt={_(ADD_LABELS.poll)} />
</button>
</>
)}
</span> </span>
<div class="spacer" /> </span>
{/* <div class="spacer" /> */}
{uiState === 'loading' ? ( {uiState === 'loading' ? (
<Loader abrupt /> <Loader abrupt />
) : ( ) : (
@ -1495,11 +1549,7 @@ function Compose({
})} })}
</select> </select>
</label>{' '} </label>{' '}
<button <button type="submit" disabled={uiState === 'loading'}>
type="submit"
class="large"
disabled={uiState === 'loading'}
>
{replyToStatus {replyToStatus
? t`Reply` ? t`Reply`
: editStatus : editStatus
@ -1671,6 +1721,58 @@ function Compose({
); );
} }
function FilePickerInput({
hidden,
supportedMimeTypes,
maxMediaAttachments,
mediaAttachments,
disabled = false,
setMediaAttachments,
}) {
return (
<input
type="file"
hidden={hidden}
accept={supportedMimeTypes?.join(',')}
multiple={
maxMediaAttachments === undefined ||
maxMediaAttachments - mediaAttachments >= 2
}
disabled={disabled}
onChange={(e) => {
const files = e.target.files;
if (!files) return;
const mediaFiles = Array.from(files).map((file) => ({
file,
type: file.type,
size: file.size,
url: URL.createObjectURL(file),
id: null, // indicate uploaded state
description: null,
}));
console.log('MEDIA ATTACHMENTS', files, mediaFiles);
// Validate max media attachments
if (mediaAttachments.length + mediaFiles.length > maxMediaAttachments) {
alert(
plural(maxMediaAttachments, {
one: 'You can only attach up to 1 file.',
other: 'You can only attach up to # files.',
}),
);
} else {
setMediaAttachments((attachments) => {
return attachments.concat(mediaFiles);
});
}
// Reset
e.target.value = '';
}}
/>
);
}
function autoResizeTextarea(textarea) { function autoResizeTextarea(textarea) {
if (!textarea) return; if (!textarea) return;
const { value, offsetHeight, scrollHeight, clientHeight } = textarea; const { value, offsetHeight, scrollHeight, clientHeight } = textarea;