this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+335 -115
+129 -11
src/components/compose.css
··· 20 20 justify-content: space-between; 21 21 gap: 8px; 22 22 align-items: center; 23 - padding: 16px; 23 + padding: 8px; 24 24 position: sticky; 25 25 top: 0; 26 26 z-index: 100; 27 27 white-space: nowrap; 28 + 29 + @media (min-width: 480px) { 30 + padding: 16px; 31 + } 28 32 } 29 33 #compose-container .compose-top .account-block { 30 34 text-align: start; ··· 110 114 } 111 115 112 116 #compose-container form { 113 - --form-padding-inline: 8px; 114 - --form-padding-block: 0; 117 + --form-spacing-inline: 4px; 118 + --form-spacing-block: 0; 115 119 /* border-radius: 16px; */ 116 - padding: var(--form-padding-block) var(--form-padding-inline); 120 + padding: var(--form-spacing-block) var(--form-spacing-inline); 117 121 background-color: var(--bg-blur-color); 118 122 /* background-image: linear-gradient(var(--bg-color) 85%, transparent); */ 119 123 position: relative; 120 124 z-index: 2; 121 125 --drop-shadow: 0 3px 6px -3px var(--drop-shadow-color); 122 126 box-shadow: var(--drop-shadow); 127 + 128 + @media (min-width: 480px) { 129 + --form-spacing-inline: 8px; 130 + } 123 131 124 132 @media (min-width: 40em) { 125 133 border-radius: 16px; ··· 153 161 display: flex; 154 162 justify-content: space-between; 155 163 align-items: center; 156 - padding: 8px 0; 157 - gap: 8px; 164 + padding: var(--form-spacing-inline) 0; 165 + gap: var(--form-spacing-inline); 158 166 } 159 167 #compose-container .toolbar.wrap { 160 168 flex-wrap: wrap; ··· 181 189 white-space: nowrap; 182 190 border: 2px solid transparent; 183 191 vertical-align: middle; 192 + 193 + &.active { 194 + filter: brightness(0.8); 195 + background-color: var(--bg-color); 196 + } 184 197 } 185 198 #compose-container .toolbar-button > * { 186 199 vertical-align: middle; ··· 248 261 max-width: 100%; 249 262 } 250 263 264 + #compose-container .compose-footer { 265 + .add-toolbar-button-group { 266 + display: flex; 267 + overflow: auto; 268 + } 269 + .add-sub-toolbar-button-group { 270 + flex-grow: 1; 271 + display: flex; 272 + overflow: auto; 273 + transition: 0.5s ease-in-out; 274 + transition-property: opacity, width; 275 + scrollbar-width: none; 276 + padding-inline-end: 16px; 277 + mask-image: linear-gradient( 278 + var(--to-backward), 279 + transparent 0, 280 + black 16px, 281 + black 100% 282 + ); 283 + 284 + &::-webkit-scrollbar { 285 + display: none; 286 + } 287 + 288 + &[hidden] { 289 + opacity: 0; 290 + pointer-events: none; 291 + width: 0; 292 + } 293 + } 294 + } 295 + 251 296 #compose-container text-expander { 252 297 position: relative; 253 298 display: block; ··· 516 561 color: var(--red-color); 517 562 } 518 563 564 + .compose-menu-add-media { 565 + position: relative; 566 + 567 + .compose-menu-add-media-field { 568 + position: absolute; 569 + inset: 0; 570 + opacity: 0; 571 + cursor: inherit; 572 + } 573 + } 574 + 575 + .icon-gif { 576 + display: inline-block !important; 577 + min-width: 16px; 578 + height: 16px; 579 + font-size: 10px !important; 580 + letter-spacing: -0.5px; 581 + font-size-adjust: none; 582 + overflow: hidden; 583 + white-space: nowrap; 584 + text-align: center; 585 + line-height: 16px; 586 + font-weight: bold; 587 + text-rendering: optimizeSpeed; 588 + 589 + &:after { 590 + display: block; 591 + content: 'GIF'; 592 + } 593 + } 594 + 519 595 @media (display-mode: standalone) { 520 596 /* No popping in standalone mode */ 521 597 #compose-container .pop-button { ··· 525 601 526 602 #compose-container button[type='submit'] { 527 603 border-radius: 8px; 604 + 528 605 @media (min-width: 480px) { 529 606 padding-inline: 24px; 607 + font-size: 125%; 530 608 } 531 609 } 532 610 ··· 820 898 .compose-field-container { 821 899 display: grid !important; 822 900 823 - @media (width < 30em) { 824 - margin-inline: calc(-1 * var(--form-padding-inline)); 901 + @media (width < 480px) { 902 + margin-inline: calc(-1 * var(--form-spacing-inline)); 825 903 width: 100vw !important; 826 904 max-width: 100vw; 827 905 ··· 929 1007 } 930 1008 } 931 1009 1010 + @keyframes jump-scare { 1011 + from { 1012 + opacity: 0.5; 1013 + transform: scale(0.25) translateX(80px); 1014 + } 1015 + to { 1016 + opacity: 1; 1017 + transform: scale(1) translateX(0); 1018 + } 1019 + } 1020 + @keyframes jump-scare-rtl { 1021 + from { 1022 + opacity: 0.5; 1023 + transform: scale(0.25) translateX(-80px); 1024 + } 1025 + to { 1026 + opacity: 1; 1027 + transform: scale(1) translateX(0); 1028 + } 1029 + } 1030 + 1031 + .add-button { 1032 + transform-origin: var(--forward) center; 1033 + background-color: var(--bg-blur-color) !important; 1034 + animation: jump-scare 0.2s ease-in-out both; 1035 + :dir(rtl) & { 1036 + animation-name: jump-scare-rtl; 1037 + } 1038 + 1039 + .icon { 1040 + transition: transform 0.3s ease-in-out; 1041 + } 1042 + &.active { 1043 + .icon { 1044 + transform: rotate(135deg); 1045 + } 1046 + } 1047 + } 1048 + 932 1049 .gif-picker-button { 933 - span { 1050 + /* span { 934 1051 font-weight: bold; 935 1052 font-size: 11.5px; 936 1053 display: block; 937 - } 1054 + line-height: 1; 1055 + } */ 938 1056 939 1057 &:is(:hover, :focus) { 940 - span { 1058 + .icon { 941 1059 animation: gif-shake 0.3s 3; 942 1060 } 943 1061 }
+206 -104
src/components/compose.jsx
··· 19 19 // import { detectAll } from 'tinyld/light'; 20 20 import { uid } from 'uid/single'; 21 21 import { useDebouncedCallback, useThrottledCallback } from 'use-debounce'; 22 + import useResizeObserver from 'use-resize-observer'; 22 23 import { useSnapshot } from 'valtio'; 23 24 24 25 import poweredByGiphyURL from '../assets/powered-by-giphy.svg'; ··· 201 202 202 203 const CUSTOM_EMOJIS_COUNT = 100; 203 204 205 + const ADD_LABELS = { 206 + media: msg`Add media`, 207 + customEmoji: msg`Add custom emoji`, 208 + gif: msg`Add GIF`, 209 + poll: msg`Add poll`, 210 + }; 211 + 204 212 function Compose({ 205 213 onClose, 206 214 replyToStatus, ··· 209 217 standalone, 210 218 hasOpener, 211 219 }) { 212 - const { i18n } = useLingui(); 220 + const { i18n, _ } = useLingui(); 213 221 const rtf = RTF(i18n.locale); 214 222 const lf = LF(i18n.locale); 215 223 ··· 732 740 states.composerState.minimized = true; 733 741 }; 734 742 743 + const gifPickerDisabled = 744 + uiState === 'loading' || 745 + (maxMediaAttachments !== undefined && 746 + mediaAttachments.length >= maxMediaAttachments) || 747 + !!poll; 748 + 749 + // If maxOptions is not defined or defined and is greater than 1, show poll button 750 + const showPollButton = maxOptions == null || maxOptions > 1; 751 + const pollButtonDisabled = 752 + uiState === 'loading' || !!poll || !!mediaAttachments.length; 753 + const onPollButtonClick = () => { 754 + setPoll({ 755 + options: ['', ''], 756 + expiresIn: 24 * 60 * 60, // 1 day 757 + multiple: false, 758 + }); 759 + }; 760 + 761 + const addSubToolbarRef = useRef(); 762 + const [showAddButton, setShowAddButton] = useState(false); 763 + useResizeObserver({ 764 + ref: addSubToolbarRef, 765 + box: 'border-box', 766 + onResize: ({ width }) => { 767 + // If scrollable, it's truncated 768 + const { scrollWidth } = addSubToolbarRef.current; 769 + const truncated = scrollWidth > width; 770 + const overTruncated = width < 84; // roughly two buttons width 771 + setShowAddButton(overTruncated || truncated); 772 + addSubToolbarRef.current.hidden = overTruncated; 773 + }, 774 + }); 775 + 735 776 return ( 736 777 <div id="compose-container-outer"> 737 778 <div id="compose-container" class={standalone ? 'standalone' : ''}> ··· 1318 1359 }} 1319 1360 /> 1320 1361 )} 1321 - <div 1322 - class="toolbar wrap" 1323 - style={{ 1324 - justifyContent: 'flex-end', 1325 - }} 1326 - > 1327 - <span> 1328 - <label class="toolbar-button"> 1329 - <input 1330 - type="file" 1331 - accept={supportedMimeTypes?.join(',')} 1332 - multiple={ 1333 - maxMediaAttachments === undefined || 1334 - maxMediaAttachments - mediaAttachments >= 2 1335 - } 1336 - disabled={ 1337 - uiState === 'loading' || 1338 - mediaAttachments.length >= maxMediaAttachments || 1339 - !!poll 1340 - } 1341 - onChange={(e) => { 1342 - const files = e.target.files; 1343 - if (!files) return; 1344 - 1345 - const mediaFiles = Array.from(files).map((file) => ({ 1346 - file, 1347 - type: file.type, 1348 - size: file.size, 1349 - url: URL.createObjectURL(file), 1350 - id: null, // indicate uploaded state 1351 - description: null, 1352 - })); 1353 - console.log('MEDIA ATTACHMENTS', files, mediaFiles); 1354 - 1355 - // Validate max media attachments 1356 - if ( 1357 - mediaAttachments.length + mediaFiles.length > 1358 - maxMediaAttachments 1359 - ) { 1360 - alert( 1361 - plural(maxMediaAttachments, { 1362 - one: 'You can only attach up to 1 file.', 1363 - other: 'You can only attach up to # files.', 1364 - }), 1365 - ); 1366 - } else { 1367 - setMediaAttachments((attachments) => { 1368 - return attachments.concat(mediaFiles); 1369 - }); 1370 - } 1371 - // Reset 1372 - e.target.value = ''; 1362 + <div class="toolbar compose-footer"> 1363 + <span class="add-toolbar-button-group spacer"> 1364 + {showAddButton && ( 1365 + <Menu2 1366 + portal={{ 1367 + target: document.body, 1368 + }} 1369 + containerProps={{ 1370 + style: { 1371 + zIndex: 1001, 1372 + }, 1373 1373 }} 1374 - /> 1375 - <Icon icon="attachment" /> 1376 - </label> 1377 - {/* If maxOptions is not defined or defined and is greater than 1, show poll button */} 1378 - {maxOptions == null || 1379 - (maxOptions > 1 && ( 1380 - <> 1374 + menuButton={({ open }) => ( 1381 1375 <button 1382 - type="button" 1383 - class="toolbar-button" 1384 - disabled={ 1385 - uiState === 'loading' || 1386 - !!poll || 1387 - !!mediaAttachments.length 1388 - } 1376 + class={`toolbar-button add-button ${ 1377 + open ? 'active' : '' 1378 + }`} 1379 + > 1380 + <Icon icon="plus" title={t`Add`} /> 1381 + </button> 1382 + )} 1383 + > 1384 + <MenuItem className="compose-menu-add-media"> 1385 + <label class="compose-menu-add-media-field"> 1386 + <FilePickerInput 1387 + hidden 1388 + supportedMimeTypes={supportedMimeTypes} 1389 + maxMediaAttachments={maxMediaAttachments} 1390 + mediaAttachments={mediaAttachments} 1391 + disabled={ 1392 + uiState === 'loading' || 1393 + mediaAttachments.length >= maxMediaAttachments || 1394 + !!poll 1395 + } 1396 + setMediaAttachments={setMediaAttachments} 1397 + /> 1398 + </label> 1399 + <Icon icon="media" /> <span>{_(ADD_LABELS.media)}</span> 1400 + </MenuItem> 1401 + <MenuItem 1402 + onClick={() => { 1403 + setShowEmoji2Picker(true); 1404 + }} 1405 + > 1406 + <Icon icon="emoji2" />{' '} 1407 + <span>{_(ADD_LABELS.customEmoji)}</span> 1408 + </MenuItem> 1409 + {!!states.settings.composerGIFPicker && ( 1410 + <MenuItem 1411 + disabled={gifPickerDisabled} 1389 1412 onClick={() => { 1390 - setPoll({ 1391 - options: ['', ''], 1392 - expiresIn: 24 * 60 * 60, // 1 day 1393 - multiple: false, 1394 - }); 1413 + setShowGIFPicker(true); 1395 1414 }} 1396 1415 > 1397 - <Icon icon="poll" alt={t`Add poll`} /> 1398 - </button> 1399 - </> 1400 - ))} 1401 - {/* <button 1416 + <span class="icon icon-gif" role="img" /> 1417 + <span>{_(ADD_LABELS.gif)}</span> 1418 + </MenuItem> 1419 + )} 1420 + <MenuItem 1421 + disabled={pollButtonDisabled} 1422 + onClick={onPollButtonClick} 1423 + > 1424 + <Icon icon="poll" /> <span>{_(ADD_LABELS.poll)}</span> 1425 + </MenuItem> 1426 + </Menu2> 1427 + )} 1428 + <span class="add-sub-toolbar-button-group" ref={addSubToolbarRef}> 1429 + <label class="toolbar-button"> 1430 + <FilePickerInput 1431 + supportedMimeTypes={supportedMimeTypes} 1432 + maxMediaAttachments={maxMediaAttachments} 1433 + mediaAttachments={mediaAttachments} 1434 + disabled={ 1435 + uiState === 'loading' || 1436 + mediaAttachments.length >= maxMediaAttachments || 1437 + !!poll 1438 + } 1439 + setMediaAttachments={setMediaAttachments} 1440 + /> 1441 + <Icon icon="media" alt={_(ADD_LABELS.media)} /> 1442 + </label> 1443 + {/* <button 1402 1444 type="button" 1403 1445 class="toolbar-button" 1404 1446 disabled={uiState === 'loading'} ··· 1408 1450 > 1409 1451 <Icon icon="at" /> 1410 1452 </button> */} 1411 - <button 1412 - type="button" 1413 - class="toolbar-button" 1414 - disabled={uiState === 'loading'} 1415 - onClick={() => { 1416 - setShowEmoji2Picker(true); 1417 - }} 1418 - > 1419 - <Icon icon="emoji2" alt={t`Add custom emoji`} /> 1420 - </button> 1421 - {!!states.settings.composerGIFPicker && ( 1422 1453 <button 1423 1454 type="button" 1424 - class="toolbar-button gif-picker-button" 1425 - disabled={ 1426 - uiState === 'loading' || 1427 - (maxMediaAttachments !== undefined && 1428 - mediaAttachments.length >= maxMediaAttachments) || 1429 - !!poll 1430 - } 1455 + class="toolbar-button" 1456 + disabled={uiState === 'loading'} 1431 1457 onClick={() => { 1432 - setShowGIFPicker(true); 1458 + setShowEmoji2Picker(true); 1433 1459 }} 1434 1460 > 1435 - <span>GIF</span> 1461 + <Icon icon="emoji2" alt={_(ADD_LABELS.customEmoji)} /> 1436 1462 </button> 1437 - )} 1463 + {!!states.settings.composerGIFPicker && ( 1464 + <button 1465 + type="button" 1466 + class="toolbar-button gif-picker-button" 1467 + disabled={gifPickerDisabled} 1468 + onClick={() => { 1469 + setShowGIFPicker(true); 1470 + }} 1471 + > 1472 + <span 1473 + class="icon icon-gif" 1474 + aria-label={_(ADD_LABELS.gif)} 1475 + /> 1476 + </button> 1477 + )} 1478 + {} 1479 + {showPollButton && ( 1480 + <> 1481 + <button 1482 + type="button" 1483 + class="toolbar-button" 1484 + disabled={pollButtonDisabled} 1485 + onClick={onPollButtonClick} 1486 + > 1487 + <Icon icon="poll" alt={_(ADD_LABELS.poll)} /> 1488 + </button> 1489 + </> 1490 + )} 1491 + </span> 1438 1492 </span> 1439 - <div class="spacer" /> 1493 + {/* <div class="spacer" /> */} 1440 1494 {uiState === 'loading' ? ( 1441 1495 <Loader abrupt /> 1442 1496 ) : ( ··· 1495 1549 })} 1496 1550 </select> 1497 1551 </label>{' '} 1498 - <button 1499 - type="submit" 1500 - class="large" 1501 - disabled={uiState === 'loading'} 1502 - > 1552 + <button type="submit" disabled={uiState === 'loading'}> 1503 1553 {replyToStatus 1504 1554 ? t`Reply` 1505 1555 : editStatus ··· 1668 1718 </Modal> 1669 1719 )} 1670 1720 </div> 1721 + ); 1722 + } 1723 + 1724 + function FilePickerInput({ 1725 + hidden, 1726 + supportedMimeTypes, 1727 + maxMediaAttachments, 1728 + mediaAttachments, 1729 + disabled = false, 1730 + setMediaAttachments, 1731 + }) { 1732 + return ( 1733 + <input 1734 + type="file" 1735 + hidden={hidden} 1736 + accept={supportedMimeTypes?.join(',')} 1737 + multiple={ 1738 + maxMediaAttachments === undefined || 1739 + maxMediaAttachments - mediaAttachments >= 2 1740 + } 1741 + disabled={disabled} 1742 + onChange={(e) => { 1743 + const files = e.target.files; 1744 + if (!files) return; 1745 + 1746 + const mediaFiles = Array.from(files).map((file) => ({ 1747 + file, 1748 + type: file.type, 1749 + size: file.size, 1750 + url: URL.createObjectURL(file), 1751 + id: null, // indicate uploaded state 1752 + description: null, 1753 + })); 1754 + console.log('MEDIA ATTACHMENTS', files, mediaFiles); 1755 + 1756 + // Validate max media attachments 1757 + if (mediaAttachments.length + mediaFiles.length > maxMediaAttachments) { 1758 + alert( 1759 + plural(maxMediaAttachments, { 1760 + one: 'You can only attach up to 1 file.', 1761 + other: 'You can only attach up to # files.', 1762 + }), 1763 + ); 1764 + } else { 1765 + setMediaAttachments((attachments) => { 1766 + return attachments.concat(mediaFiles); 1767 + }); 1768 + } 1769 + // Reset 1770 + e.target.value = ''; 1771 + }} 1772 + /> 1671 1773 ); 1672 1774 } 1673 1775