fix: schedule button, compression visibility, activity log, insights overflow, bookmarks UI
Schedule message: modal now always opens (even with empty composer); includes its own message textarea pre-filled from editor content; removed null-content early-return guard from handleScheduleClick; fixed error text to use color.Critical.Main not CSS var Image compression: removed 200KB size threshold — checkbox now shows for all JPEG/PNG uploads (not just large ones); 'no significant saving' message handles already-small files gracefully Activity log: auto-paginate on mount — state events are absent from initial sync window, so the log was always empty until Load More was clicked manually Insights summary: Text size H4→H5 with toLocaleString() formatting and overflow:ellipsis so large numbers don't push tiles off screen Bookmarks panel: replaced var(--bg-*) CSS vars (undefined in folds themes) with color.Surface/SurfaceVariant/Primary folds tokens; added left accent border on message preview block, count badge, clear button in search, improved empty state, cleaner button hierarchy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,17 @@
|
|||||||
import React, { ChangeEvent, useState } from 'react';
|
import React, { ChangeEvent, useState } from 'react';
|
||||||
import { Box, Button, Header, Icon, IconButton, Icons, Input, Scroll, Text, config } from 'folds';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Header,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Icons,
|
||||||
|
Input,
|
||||||
|
Scroll,
|
||||||
|
Text,
|
||||||
|
color,
|
||||||
|
config,
|
||||||
|
} from 'folds';
|
||||||
import { useBookmarks, Bookmark } from '../../hooks/useBookmarks';
|
import { useBookmarks, Bookmark } from '../../hooks/useBookmarks';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||||
@@ -14,8 +26,6 @@ function formatTimeAgo(ts: number): string {
|
|||||||
const days = Math.floor(hours / 24);
|
const days = Math.floor(hours / 24);
|
||||||
if (days === 1) return 'yesterday';
|
if (days === 1) return 'yesterday';
|
||||||
if (days < 7) return `${days}d ago`;
|
if (days < 7) return `${days}d ago`;
|
||||||
const weeks = Math.floor(days / 7);
|
|
||||||
if (weeks < 5) return `${weeks}w ago`;
|
|
||||||
return new Date(ts).toLocaleDateString();
|
return new Date(ts).toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,65 +40,83 @@ function BookmarkItem({ bookmark, onJump, onRemove }: BookmarkItemProps) {
|
|||||||
const room = mx.getRoom(bookmark.roomId);
|
const room = mx.getRoom(bookmark.roomId);
|
||||||
const displayRoomName = room?.name ?? bookmark.roomName;
|
const displayRoomName = room?.name ?? bookmark.roomName;
|
||||||
|
|
||||||
const handleJump = () => {
|
|
||||||
onJump(bookmark.roomId, bookmark.eventId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemove = () => {
|
|
||||||
onRemove(bookmark.eventId);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="100"
|
gap="200"
|
||||||
style={{
|
style={{
|
||||||
padding: `${config.space.S200} ${config.space.S300}`,
|
padding: `${config.space.S300} ${config.space.S300}`,
|
||||||
borderBottomWidth: config.borderWidth.B300,
|
borderBottom: `1px solid ${color.Surface.ContainerLine}`,
|
||||||
borderBottomStyle: 'solid',
|
background: color.Surface.Container,
|
||||||
borderBottomColor: 'var(--bg-surface-border)',
|
transition: 'background 0.1s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="L400" truncate>
|
{/* Room name row */}
|
||||||
{displayRoomName}
|
<Box alignItems="Center" gap="100">
|
||||||
</Text>
|
<Icon src={Icons.Hash} size="50" style={{ opacity: 0.5, flexShrink: 0 }} />
|
||||||
<Text
|
<Text
|
||||||
size="T300"
|
size="T200"
|
||||||
priority="300"
|
style={{
|
||||||
|
color: color.Primary.Main,
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displayRoomName}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Message preview */}
|
||||||
|
<Box
|
||||||
style={{
|
style={{
|
||||||
overflow: 'hidden',
|
background: color.SurfaceVariant.Container,
|
||||||
display: '-webkit-box',
|
borderRadius: config.radii.R300,
|
||||||
WebkitLineClamp: 2,
|
borderLeft: `3px solid ${color.Primary.Main}`,
|
||||||
WebkitBoxOrient: 'vertical',
|
padding: `${config.space.S100} ${config.space.S200}`,
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{bookmark.previewText || '(no preview)'}
|
<Text
|
||||||
</Text>
|
size="T300"
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
opacity: 0.9,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{bookmark.previewText || '(no preview)'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Footer row */}
|
||||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
||||||
<Text size="T200" priority="300">
|
<Text size="T200" style={{ opacity: 0.5 }}>
|
||||||
Saved {formatTimeAgo(bookmark.savedAt)}
|
{formatTimeAgo(bookmark.savedAt)}
|
||||||
</Text>
|
</Text>
|
||||||
<Box gap="100" shrink="No">
|
<Box gap="100" shrink="No">
|
||||||
<Button
|
<Button
|
||||||
size="300"
|
size="300"
|
||||||
variant="Secondary"
|
variant="Primary"
|
||||||
fill="Soft"
|
fill="Soft"
|
||||||
radii="300"
|
radii="300"
|
||||||
onClick={handleJump}
|
onClick={() => onJump(bookmark.roomId, bookmark.eventId)}
|
||||||
before={<Icon size="100" src={Icons.ArrowRight} />}
|
before={<Icon size="100" src={Icons.ArrowRight} />}
|
||||||
>
|
>
|
||||||
<Text size="T300">Jump</Text>
|
<Text size="T300">Jump</Text>
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="300"
|
size="300"
|
||||||
variant="Critical"
|
variant="Surface"
|
||||||
fill="None"
|
fill="None"
|
||||||
radii="300"
|
radii="300"
|
||||||
onClick={handleRemove}
|
onClick={() => onRemove(bookmark.eventId)}
|
||||||
aria-label="Remove bookmark"
|
aria-label="Remove bookmark"
|
||||||
>
|
>
|
||||||
<Icon size="100" src={Icons.Cross} />
|
<Icon size="100" src={Icons.Delete} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -129,22 +157,21 @@ export function BookmarksPanel({ onClose }: BookmarksPanelProps) {
|
|||||||
width: '266px',
|
width: '266px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderLeftWidth: config.borderWidth.B300,
|
borderLeft: `1px solid ${color.Surface.ContainerLine}`,
|
||||||
borderLeftStyle: 'solid',
|
background: color.Surface.Container,
|
||||||
borderLeftColor: 'var(--bg-surface-border)',
|
|
||||||
backgroundColor: 'var(--bg-background)',
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Header */}
|
||||||
<Header
|
<Header
|
||||||
style={{
|
style={{
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
|
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
|
||||||
borderBottomWidth: config.borderWidth.B300,
|
borderBottom: `1px solid ${color.Surface.ContainerLine}`,
|
||||||
}}
|
}}
|
||||||
variant="Background"
|
variant="Surface"
|
||||||
size="600"
|
size="600"
|
||||||
>
|
>
|
||||||
<Box grow="Yes" alignItems="Center" gap="200">
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
@@ -152,44 +179,84 @@ export function BookmarksPanel({ onClose }: BookmarksPanelProps) {
|
|||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H5">Saved Messages</Text>
|
<Text size="H5">Saved Messages</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton variant="Background" aria-label="Close saved messages" onClick={onClose}>
|
<IconButton
|
||||||
<Icon src={Icons.Cross} />
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
aria-label="Close saved messages"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<Icon src={Icons.Cross} size="100" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
padding: config.space.S200,
|
padding: config.space.S200,
|
||||||
borderBottomWidth: config.borderWidth.B300,
|
borderBottom: `1px solid ${color.Surface.ContainerLine}`,
|
||||||
borderBottomStyle: 'solid',
|
background: color.SurfaceVariant.Container,
|
||||||
borderBottomColor: 'var(--bg-surface-border)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
variant="Background"
|
variant="Surface"
|
||||||
placeholder="Filter saved messages..."
|
size="400"
|
||||||
|
radii="400"
|
||||||
|
placeholder="Search saved messages…"
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
before={<Icon size="200" src={Icons.Search} />}
|
before={<Icon size="200" src={Icons.Search} />}
|
||||||
|
after={
|
||||||
|
filter.length > 0 ? (
|
||||||
|
<IconButton
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
aria-label="Clear search"
|
||||||
|
onClick={() => setFilter('')}
|
||||||
|
>
|
||||||
|
<Icon size="100" src={Icons.Cross} />
|
||||||
|
</IconButton>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Count badge */}
|
||||||
|
{bookmarks.length > 0 && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
padding: `${config.space.S100} ${config.space.S300}`,
|
||||||
|
borderBottom: `1px solid ${color.Surface.ContainerLine}`,
|
||||||
|
background: color.SurfaceVariant.Container,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="T200" style={{ opacity: 0.6 }}>
|
||||||
|
{filtered.length === bookmarks.length
|
||||||
|
? `${bookmarks.length} saved message${bookmarks.length !== 1 ? 's' : ''}`
|
||||||
|
: `${filtered.length} of ${bookmarks.length} messages`}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* List */}
|
||||||
<Scroll variant="Background" size="300" style={{ flexGrow: 1, minHeight: 0 }}>
|
<Scroll variant="Background" size="300" style={{ flexGrow: 1, minHeight: 0 }}>
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<Box
|
<Box
|
||||||
direction="Column"
|
direction="Column"
|
||||||
alignItems="Center"
|
alignItems="Center"
|
||||||
justifyContent="Center"
|
justifyContent="Center"
|
||||||
gap="200"
|
gap="300"
|
||||||
style={{ padding: config.space.S500 }}
|
style={{ padding: config.space.S600, textAlign: 'center' }}
|
||||||
>
|
>
|
||||||
<Icon size="600" src={Icons.Star} />
|
<Icon size="600" src={Icons.Star} style={{ opacity: 0.3 }} />
|
||||||
<Text size="T300" priority="300" align="Center">
|
<Text size="T300" priority="300" align="Center">
|
||||||
{bookmarks.length === 0
|
{bookmarks.length === 0
|
||||||
? 'No saved messages yet. Right-click any message to bookmark it.'
|
? 'No saved messages yet.\nRight-click any message to bookmark it.'
|
||||||
: 'No bookmarks match your filter.'}
|
: 'No bookmarks match your search.'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -316,6 +316,13 @@ export function RoomActivityLog({ requestClose }: RoomActivityLogProps) {
|
|||||||
setEvents(getStateEvents());
|
setEvents(getStateEvents());
|
||||||
}, [getStateEvents]);
|
}, [getStateEvents]);
|
||||||
|
|
||||||
|
// Auto-paginate on mount — state events are rarely in the initial sync
|
||||||
|
// window, so we immediately fetch backwards to populate the log.
|
||||||
|
useEffect(() => {
|
||||||
|
handleLoadMore();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleLoadMore = useCallback(async () => {
|
const handleLoadMore = useCallback(async () => {
|
||||||
if (loading || !canLoadMore) return;
|
if (loading || !canLoadMore) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -208,8 +208,17 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) {
|
|||||||
background: color.Surface.Container,
|
background: color.Surface.Container,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="H4" style={{ fontWeight: 700 }}>
|
<Text
|
||||||
{stats.totalMessages}
|
size="H5"
|
||||||
|
style={{
|
||||||
|
fontWeight: 700,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
maxWidth: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stats.totalMessages.toLocaleString()}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="T200" priority="300" align="Center">
|
<Text size="T200" priority="300" align="Center">
|
||||||
Messages
|
Messages
|
||||||
@@ -228,8 +237,17 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) {
|
|||||||
background: color.Surface.Container,
|
background: color.Surface.Container,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="H4" style={{ fontWeight: 700 }}>
|
<Text
|
||||||
{stats.uniqueParticipants}
|
size="H5"
|
||||||
|
style={{
|
||||||
|
fontWeight: 700,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
maxWidth: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stats.uniqueParticipants.toLocaleString()}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="T200" priority="300" align="Center">
|
<Text size="T200" priority="300" align="Center">
|
||||||
Participants
|
Participants
|
||||||
@@ -248,8 +266,17 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) {
|
|||||||
background: color.Surface.Container,
|
background: color.Surface.Container,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="H4" style={{ fontWeight: 700 }}>
|
<Text
|
||||||
{stats.totalCached}
|
size="H5"
|
||||||
|
style={{
|
||||||
|
fontWeight: 700,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
maxWidth: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stats.totalCached.toLocaleString()}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="T200" priority="300" align="Center">
|
<Text size="T200" priority="300" align="Center">
|
||||||
Cached events
|
Cached events
|
||||||
|
|||||||
@@ -598,8 +598,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
}, [editor, isMarkdown, mx, roomId, replyDraft]);
|
}, [editor, isMarkdown, mx, roomId, replyDraft]);
|
||||||
|
|
||||||
const handleScheduleClick = useCallback(() => {
|
const handleScheduleClick = useCallback(() => {
|
||||||
|
// Pre-fill from editor if there's content; open blank if editor is empty.
|
||||||
const content = buildCurrentTextContent();
|
const content = buildCurrentTextContent();
|
||||||
if (!content) return;
|
|
||||||
setScheduleContent(content);
|
setScheduleContent(content);
|
||||||
setScheduleOpen(true);
|
setScheduleOpen(true);
|
||||||
}, [buildCurrentTextContent]);
|
}, [buildCurrentTextContent]);
|
||||||
@@ -1171,10 +1171,12 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{pollOpen && <PollCreator room={room} roomId={roomId} onClose={() => setPollOpen(false)} />}
|
{pollOpen && <PollCreator room={room} roomId={roomId} onClose={() => setPollOpen(false)} />}
|
||||||
{scheduleOpen && scheduleContent && (
|
{scheduleOpen && (
|
||||||
<ScheduleMessageModal
|
<ScheduleMessageModal
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
content={scheduleContent}
|
initialBody={
|
||||||
|
typeof scheduleContent?.body === 'string' ? scheduleContent.body : undefined
|
||||||
|
}
|
||||||
onScheduled={handleScheduled}
|
onScheduled={handleScheduled}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setScheduleOpen(false);
|
setScheduleOpen(false);
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import { scheduleMessage } from '../../utils/scheduledMessages';
|
|||||||
|
|
||||||
interface ScheduleMessageModalProps {
|
interface ScheduleMessageModalProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
content: IContent;
|
/** Pre-fill the message body from the composer. Pass null/undefined to open blank. */
|
||||||
|
initialBody?: string;
|
||||||
onScheduled: (delayId: string, sendAt: number, content: IContent) => void;
|
onScheduled: (delayId: string, sendAt: number, content: IContent) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
@@ -66,11 +67,12 @@ function toLocalDatetimeValue(date: Date): string {
|
|||||||
|
|
||||||
export function ScheduleMessageModal({
|
export function ScheduleMessageModal({
|
||||||
roomId,
|
roomId,
|
||||||
content,
|
initialBody,
|
||||||
onScheduled,
|
onScheduled,
|
||||||
onClose,
|
onClose,
|
||||||
}: ScheduleMessageModalProps) {
|
}: ScheduleMessageModalProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
const [messageText, setMessageText] = useState(initialBody ?? '');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -126,6 +128,12 @@ export function ScheduleMessageModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!messageText.trim()) {
|
||||||
|
setError('Please enter a message to schedule.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: IContent = { body: messageText.trim(), msgtype: 'm.text' };
|
||||||
setError(null);
|
setError(null);
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
@@ -187,31 +195,32 @@ export function ScheduleMessageModal({
|
|||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
|
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
|
||||||
{/* Message preview */}
|
{/* Message input */}
|
||||||
{typeof content.body === 'string' && content.body.trim() !== '' && (
|
<Box direction="Column" gap="100">
|
||||||
<Box
|
<Text as="label" htmlFor="schedule-message-body" size="L400">
|
||||||
direction="Column"
|
Message
|
||||||
gap="100"
|
</Text>
|
||||||
|
<textarea
|
||||||
|
id="schedule-message-body"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Type your message here…"
|
||||||
|
value={messageText}
|
||||||
|
onChange={(e) => setMessageText(e.target.value)}
|
||||||
style={{
|
style={{
|
||||||
background: color.SurfaceVariant.Container,
|
background: color.SurfaceVariant.Container,
|
||||||
|
color: color.SurfaceVariant.OnContainer,
|
||||||
|
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||||
borderRadius: config.radii.R300,
|
borderRadius: config.radii.R300,
|
||||||
padding: config.space.S200,
|
padding: `${config.space.S200} ${config.space.S300}`,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
width: '100%',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
outline: 'none',
|
||||||
|
resize: 'vertical',
|
||||||
|
fontFamily: 'inherit',
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Text size="L400">Message</Text>
|
</Box>
|
||||||
<Text
|
|
||||||
size="T300"
|
|
||||||
style={{
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
opacity: 0.8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{content.body as string}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Datetime picker */}
|
{/* Datetime picker */}
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
@@ -249,7 +258,7 @@ export function ScheduleMessageModal({
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
datetimeValue && (
|
datetimeValue && (
|
||||||
<Text size="T200" style={{ color: 'var(--tc-danger-normal)' }}>
|
<Text size="T200" style={{ color: color.Critical.Main }}>
|
||||||
Must be at least 1 minute in the future
|
Must be at least 1 minute in the future
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
@@ -257,7 +266,7 @@ export function ScheduleMessageModal({
|
|||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{error && (
|
{error && (
|
||||||
<Text size="T300" style={{ color: 'var(--tc-danger-normal)' }}>
|
<Text size="T300" style={{ color: color.Critical.Main }}>
|
||||||
{error}
|
{error}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ export type CompressionResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const COMPRESSIBLE_TYPES = ['image/jpeg', 'image/png'];
|
const COMPRESSIBLE_TYPES = ['image/jpeg', 'image/png'];
|
||||||
const COMPRESSION_SKIP_THRESHOLD = 200 * 1024; // 200 KB
|
|
||||||
|
|
||||||
/** Returns true if this file type can be compressed AND the file is large enough to bother. */
|
/** Returns true if this file type can be compressed (JPEG or PNG, any size). */
|
||||||
export function isCompressible(file: File): boolean {
|
export function isCompressible(file: File): boolean {
|
||||||
return COMPRESSIBLE_TYPES.includes(file.type) && file.size >= COMPRESSION_SKIP_THRESHOLD;
|
return COMPRESSIBLE_TYPES.includes(file.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user