Add Stationery and Stationery Dark themes with notebook chrome.
Manila folders, ruled chat, post-it nav, and folder tabs; dark variant uses Catppuccin Mocha colors with muted stickies and soft glows.
This commit is contained in:
16
src/app/features/room/RoomInput.css.ts
Normal file
16
src/app/features/room/RoomInput.css.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
import { color, config } from 'folds';
|
||||
import * as editorCss from '../../components/editor/Editor.css';
|
||||
|
||||
export const RoomInputWrap = style({
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
globalStyle(`${RoomInputWrap} .${editorCss.Editor}`, {
|
||||
borderRadius: 0,
|
||||
boxShadow: 'none',
|
||||
borderTop: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderLeft: 'none',
|
||||
borderRight: 'none',
|
||||
borderBottom: 'none',
|
||||
});
|
||||
@@ -108,6 +108,18 @@ import { useComposingCheck } from '../../hooks/useComposingCheck';
|
||||
import { useOtherUserColor } from '../../hooks/useUserColor';
|
||||
import { getMemberAvatarMxc } from '../../utils/room';
|
||||
import { convertEmoticons, convertEmoticonsInHtml } from '../../utils/emoticonConverter';
|
||||
import * as css from './RoomInput.css';
|
||||
|
||||
/**
|
||||
* Creates a UUID used to group image events into one carousel.
|
||||
*/
|
||||
const createCarouselUuid = (): string => {
|
||||
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
|
||||
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
};
|
||||
|
||||
interface RoomInputProps {
|
||||
editor: Editor;
|
||||
@@ -306,12 +318,34 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
};
|
||||
|
||||
const handleSendUpload = async (uploads: UploadSuccess[]) => {
|
||||
const imageUploads = uploads.filter((upload) => {
|
||||
const fileItem = selectedFiles.find((f) => f.file === upload.file);
|
||||
return !!fileItem && fileItem.file.type.startsWith('image');
|
||||
});
|
||||
const carouselUuid = imageUploads.length > 1 ? createCarouselUuid() : undefined;
|
||||
const imageUploadIndexByFile = new Map(
|
||||
imageUploads.map((upload, index) => [upload.file, index] as const)
|
||||
);
|
||||
|
||||
const contentsPromises = uploads.map(async (upload) => {
|
||||
const fileItem = selectedFiles.find((f) => f.file === upload.file);
|
||||
if (!fileItem) throw new Error('Broken upload');
|
||||
|
||||
if (fileItem.file.type.startsWith('image')) {
|
||||
return getImageMsgContent(mx, fileItem, upload.mxc);
|
||||
const imageIndex = imageUploadIndexByFile.get(upload.file);
|
||||
|
||||
return getImageMsgContent(
|
||||
mx,
|
||||
fileItem,
|
||||
upload.mxc,
|
||||
carouselUuid !== undefined && imageIndex !== undefined
|
||||
? {
|
||||
uuid: carouselUuid,
|
||||
index: imageIndex,
|
||||
total: imageUploads.length,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
if (fileItem.file.type.startsWith('video')) {
|
||||
return getVideoMsgContent(mx, fileItem, upload.mxc);
|
||||
@@ -584,7 +618,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div ref={ref} className={css.RoomInputWrap}>
|
||||
{selectedFiles.length > 0 && (
|
||||
<UploadBoard
|
||||
header={
|
||||
@@ -690,8 +724,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
backgroundColor: color.SurfaceVariant.Container,
|
||||
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderBottom: 'none',
|
||||
borderTopLeftRadius: config.radii.R400,
|
||||
borderTopRightRadius: config.radii.R400,
|
||||
borderRadius: 0,
|
||||
marginBottom: config.space.S100,
|
||||
}}
|
||||
direction="Column"
|
||||
|
||||
@@ -5,6 +5,6 @@ export const RoomInputPlaceholder = style({
|
||||
minHeight: toRem(48),
|
||||
backgroundColor: color.SurfaceVariant.Container,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
boxShadow: `inset 0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderRadius: config.radii.R400,
|
||||
borderTop: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderRadius: 0,
|
||||
});
|
||||
|
||||
@@ -2421,6 +2421,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
ref={timelineContentRef}
|
||||
direction="Column"
|
||||
justifyContent="End"
|
||||
data-chatroll=""
|
||||
style={{
|
||||
minHeight: '100%',
|
||||
padding: `${config.space.S600} 0`,
|
||||
|
||||
@@ -14,7 +14,8 @@ import { RoomTimeline } from './RoomTimeline';
|
||||
import { RoomViewTyping } from './RoomViewTyping';
|
||||
import { RoomTombstone } from './RoomTombstone';
|
||||
import { RoomInput } from './RoomInput';
|
||||
import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
|
||||
import { RoomViewFollowing } from './RoomViewFollowing';
|
||||
import * as roomViewCss from './RoomViewFollowing.css';
|
||||
import { Page } from '../../components/page';
|
||||
import { RoomViewHeader } from './RoomViewHeader';
|
||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||
@@ -109,7 +110,7 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
||||
<ThreadView room={room} threadRootId={activeThreadId} />
|
||||
) : (
|
||||
<>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Box grow="Yes" direction="Column" style={{ position: 'relative', minHeight: 0 }}>
|
||||
<RoomTimeline
|
||||
key={roomId}
|
||||
room={room}
|
||||
@@ -117,40 +118,42 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
||||
roomInputRef={roomInputRef}
|
||||
editor={editor}
|
||||
/>
|
||||
<RoomViewTyping room={room} />
|
||||
<div className={roomViewCss.RoomViewBottomFloat}>
|
||||
<RoomViewTyping room={room} />
|
||||
{!hideActivity && <RoomViewFollowing room={room} />}
|
||||
</div>
|
||||
</Box>
|
||||
<Box shrink="No" direction="Column">
|
||||
<div style={{ padding: `0 ${config.space.S400}` }}>
|
||||
{tombstoneEvent ? (
|
||||
{tombstoneEvent ? (
|
||||
<div style={{ padding: `0 ${config.space.S400}` }}>
|
||||
<RoomTombstone
|
||||
roomId={roomId}
|
||||
body={tombstoneEvent.getContent().body}
|
||||
replacementRoomId={tombstoneEvent.getContent().replacement_room}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{canMessage && (
|
||||
<RoomInput
|
||||
room={room}
|
||||
editor={editor}
|
||||
roomId={roomId}
|
||||
fileDropContainerRef={roomViewRef}
|
||||
ref={roomInputRef}
|
||||
/>
|
||||
)}
|
||||
{!canMessage && (
|
||||
<RoomInputPlaceholder
|
||||
style={{ padding: config.space.S200 }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
>
|
||||
<Text align="Center">You do not have permission to post in this room</Text>
|
||||
</RoomInputPlaceholder>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{canMessage && (
|
||||
<RoomInput
|
||||
room={room}
|
||||
editor={editor}
|
||||
roomId={roomId}
|
||||
fileDropContainerRef={roomViewRef}
|
||||
ref={roomInputRef}
|
||||
/>
|
||||
)}
|
||||
{!canMessage && (
|
||||
<RoomInputPlaceholder
|
||||
style={{ padding: config.space.S200 }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
>
|
||||
<Text align="Center">You do not have permission to post in this room</Text>
|
||||
</RoomInputPlaceholder>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,20 @@ import { style } from '@vanilla-extract/css';
|
||||
import { recipe } from '@vanilla-extract/recipes';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
|
||||
/** Floats typing + read receipts over the bottom of the timeline. */
|
||||
export const RoomViewBottomFloat = style({
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: config.space.S200,
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const RoomViewFollowingPlaceholder = style([
|
||||
DefaultReset,
|
||||
{
|
||||
@@ -15,10 +29,12 @@ export const RoomViewFollowing = recipe({
|
||||
{
|
||||
minHeight: toRem(28),
|
||||
padding: `0 ${config.space.S400}`,
|
||||
width: '100%',
|
||||
backgroundColor: color.Surface.Container,
|
||||
marginLeft: 'auto',
|
||||
maxWidth: '50%',
|
||||
backgroundColor: 'transparent',
|
||||
color: color.Surface.OnContainer,
|
||||
outline: 'none',
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
],
|
||||
variants: {
|
||||
|
||||
@@ -35,6 +35,10 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
|
||||
|
||||
const eventId = latestEvent?.getId();
|
||||
|
||||
if (names.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{eventId && (
|
||||
@@ -56,64 +60,58 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
|
||||
</Overlay>
|
||||
)}
|
||||
<Box
|
||||
as={names.length > 0 ? 'button' : 'div'}
|
||||
onClick={names.length > 0 ? () => setOpen(true) : undefined}
|
||||
className={classNames(css.RoomViewFollowing({ clickable: names.length > 0 }), className)}
|
||||
as="button"
|
||||
onClick={() => setOpen(true)}
|
||||
className={classNames(css.RoomViewFollowing({ clickable: true }), className)}
|
||||
alignItems="Center"
|
||||
justifyContent="End"
|
||||
gap="200"
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{names.length > 0 && (
|
||||
<>
|
||||
<Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.CheckTwice} />
|
||||
<Text size="T300" truncate>
|
||||
{names.length === 1 && (
|
||||
<b>{names[0]}</b>
|
||||
)}
|
||||
{names.length === 2 && (
|
||||
<>
|
||||
<b>{names[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{names[1]}</b>
|
||||
</>
|
||||
)}
|
||||
{names.length === 3 && (
|
||||
<>
|
||||
<b>{names[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{names[1]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{names[2]}</b>
|
||||
</>
|
||||
)}
|
||||
{names.length > 3 && (
|
||||
<>
|
||||
<b>{names[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{names[1]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{names[2]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{names.length - 3} others</b>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.CheckTwice} />
|
||||
<Text size="T300" truncate>
|
||||
{names.length === 1 && <b>{names[0]}</b>}
|
||||
{names.length === 2 && (
|
||||
<>
|
||||
<b>{names[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{names[1]}</b>
|
||||
</>
|
||||
)}
|
||||
{names.length === 3 && (
|
||||
<>
|
||||
<b>{names[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{names[1]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{names[2]}</b>
|
||||
</>
|
||||
)}
|
||||
{names.length > 3 && (
|
||||
<>
|
||||
<b>{names[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{names[1]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{names[2]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{names.length - 3} others</b>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,9 @@ import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
|
||||
import { getCanonicalAliasOrRoomId, isRoomAlias, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { getCanonicalAliasOrRoomId, guessDmRoomUserId, isRoomAlias, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { useOtherUserColor } from '../../hooks/useUserColor';
|
||||
import { _SearchPathSearchParams } from '../../pages/paths';
|
||||
import * as css from './RoomViewHeader.css';
|
||||
import { useRoomUnread } from '../../state/hooks/unread';
|
||||
@@ -473,13 +475,23 @@ export function RoomViewHeader({ forumLayout = false }: RoomViewHeaderProps) {
|
||||
const pinnedEvents = useRoomPinnedEvents(room);
|
||||
const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption);
|
||||
const ecryptedRoom = !!encryptionEvent;
|
||||
const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
|
||||
const isDirect = mDirects.has(room.roomId);
|
||||
const avatarMxc = useRoomAvatar(room, isDirect);
|
||||
const name = useRoomName(room);
|
||||
const topic = useRoomTopic(room);
|
||||
const avatarUrl = avatarMxc
|
||||
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
const dmUserId = isDirect ? guessDmRoomUserId(room, mx.getSafeUserId()) : undefined;
|
||||
const dmAvatarMxc =
|
||||
(dmUserId ? room.getMember(dmUserId)?.getMxcAvatarUrl() : undefined) ||
|
||||
(isDirect ? avatarMxc : undefined);
|
||||
const dmCustomColor = useOtherUserColor(dmUserId ?? '', dmAvatarMxc);
|
||||
const dmFolderTabColor = isDirect
|
||||
? `color-mix(in srgb, ${dmCustomColor ?? colorMXID(dmUserId ?? room.roomId)} 50%, var(--folder-tab-room, #f3e6c8))`
|
||||
: undefined;
|
||||
|
||||
const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [activeThreadId, setActiveThreadId] = useAtom(activeThreadIdAtomFamily(room.roomId));
|
||||
|
||||
@@ -511,7 +523,24 @@ export function RoomViewHeader({ forumLayout = false }: RoomViewHeaderProps) {
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
<Box grow="Yes" alignItems="Center" gap="300">
|
||||
<Box
|
||||
data-folder-tab="room"
|
||||
data-folder-tab-dm={isDirect ? '' : undefined}
|
||||
shrink="No"
|
||||
alignItems="Center"
|
||||
gap="300"
|
||||
style={{
|
||||
minWidth: 0,
|
||||
maxWidth: '100%',
|
||||
...(dmFolderTabColor
|
||||
? {
|
||||
['--folder-tab-fill' as string]: dmFolderTabColor,
|
||||
background: dmFolderTabColor,
|
||||
backgroundColor: dmFolderTabColor,
|
||||
}
|
||||
: undefined),
|
||||
}}
|
||||
>
|
||||
{!showInPageHeader && (
|
||||
<Avatar size="300">
|
||||
<RoomAvatar
|
||||
@@ -528,7 +557,7 @@ export function RoomViewHeader({ forumLayout = false }: RoomViewHeaderProps) {
|
||||
/>
|
||||
</Avatar>
|
||||
)}
|
||||
<Box direction="Column">
|
||||
<Box direction="Column" style={{ minWidth: 0 }}>
|
||||
<Box alignItems="Center" gap="100">
|
||||
<Text size={topic ? 'H5' : 'H3'} truncate>
|
||||
{name}
|
||||
@@ -578,6 +607,7 @@ export function RoomViewHeader({ forumLayout = false }: RoomViewHeaderProps) {
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box grow="Yes" />
|
||||
<Box shrink="No" alignItems="Center" gap="100">
|
||||
<CallIndicator roomId={room.roomId} />
|
||||
<RoomCallButtons roomId={room.roomId} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
import { DefaultReset, color, config } from 'folds';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
|
||||
const SlideUpAnime = keyframes({
|
||||
from: {
|
||||
@@ -13,15 +13,17 @@ const SlideUpAnime = keyframes({
|
||||
export const RoomViewTyping = style([
|
||||
DefaultReset,
|
||||
{
|
||||
padding: `0 ${config.space.S500}`,
|
||||
width: '100%',
|
||||
backgroundColor: color.Surface.Container,
|
||||
minHeight: toRem(28),
|
||||
padding: `0 ${config.space.S400}`,
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
backgroundColor: 'transparent',
|
||||
color: color.Surface.OnContainer,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
pointerEvents: 'auto',
|
||||
animation: `${SlideUpAnime} 100ms ease-in-out`,
|
||||
},
|
||||
]);
|
||||
|
||||
export const TypingText = style({
|
||||
flexGrow: 1,
|
||||
overflow: 'clip',
|
||||
|
||||
@@ -45,78 +45,76 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Box
|
||||
className={classNames(css.RoomViewTyping, className)}
|
||||
alignItems="Center"
|
||||
gap="400"
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<TypingIndicator />
|
||||
<Text className={css.TypingText} size="T300" truncate>
|
||||
{typingNames.length === 1 && (
|
||||
<>
|
||||
<b>{typingNames[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' is typing...'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{typingNames.length === 2 && (
|
||||
<>
|
||||
<b>{typingNames[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{typingNames[1]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' are typing...'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{typingNames.length === 3 && (
|
||||
<>
|
||||
<b>{typingNames[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{typingNames[1]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{typingNames[2]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' are typing...'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{typingNames.length > 3 && (
|
||||
<>
|
||||
<b>{typingNames[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{typingNames[1]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{typingNames[2]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{typingNames.length - 3} others</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' are typing...'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<IconButton title="Drop Typing Status" size="300" radii="Pill" onClick={handleDropAll}>
|
||||
<Icon size="50" src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</div>
|
||||
<Box
|
||||
className={classNames(css.RoomViewTyping, className)}
|
||||
alignItems="Center"
|
||||
gap="400"
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<TypingIndicator />
|
||||
<Text className={css.TypingText} size="T300" truncate>
|
||||
{typingNames.length === 1 && (
|
||||
<>
|
||||
<b>{typingNames[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' is typing...'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{typingNames.length === 2 && (
|
||||
<>
|
||||
<b>{typingNames[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{typingNames[1]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' are typing...'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{typingNames.length === 3 && (
|
||||
<>
|
||||
<b>{typingNames[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{typingNames[1]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{typingNames[2]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' are typing...'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{typingNames.length > 3 && (
|
||||
<>
|
||||
<b>{typingNames[0]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{typingNames[1]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{', '}
|
||||
</Text>
|
||||
<b>{typingNames[2]}</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' and '}
|
||||
</Text>
|
||||
<b>{typingNames.length - 3} others</b>
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{' are typing...'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<IconButton title="Drop Typing Status" size="300" radii="Pill" onClick={handleDropAll}>
|
||||
<Icon size="50" src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -60,7 +60,8 @@ import { activeThreadIdAtomFamily } from '../../state/activeThread';
|
||||
import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
|
||||
import { RoomInput } from './RoomInput';
|
||||
import { RoomViewTyping } from './RoomViewTyping';
|
||||
import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
|
||||
import { RoomViewFollowing } from './RoomViewFollowing';
|
||||
import * as roomViewCss from './RoomViewFollowing.css';
|
||||
import { Message, Reactions, EncryptedContent } from './message';
|
||||
import { Reply } from '../../components/message';
|
||||
import { RenderMessageContent } from '../../components/RenderMessageContent';
|
||||
@@ -731,8 +732,8 @@ export function ThreadView({ room, threadRootId }: ThreadViewProps) {
|
||||
</Box>
|
||||
|
||||
{/* Thread messages — grows to fill remaining space */}
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Box grow="Yes" style={{ position: 'relative' }} ref={containerRef}>
|
||||
<Box grow="Yes" direction="Column" style={{ position: 'relative', minHeight: 0 }}>
|
||||
<Box grow="Yes" style={{ position: 'relative', minHeight: 0 }} ref={containerRef}>
|
||||
<Scroll ref={scrollRef} visibility="Hover">
|
||||
<Box
|
||||
direction="Column"
|
||||
@@ -759,22 +760,22 @@ export function ThreadView({ room, threadRootId }: ThreadViewProps) {
|
||||
</Box>
|
||||
</Scroll>
|
||||
</Box>
|
||||
<RoomViewTyping room={room} />
|
||||
<div className={roomViewCss.RoomViewBottomFloat}>
|
||||
<RoomViewTyping room={room} />
|
||||
{!hideActivity && <RoomViewFollowing room={room} />}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{/* Thread input */}
|
||||
<Box shrink="No" direction="Column">
|
||||
<div style={{ padding: `0 ${config.space.S400}` }}>
|
||||
<RoomInput
|
||||
room={room}
|
||||
editor={editor}
|
||||
roomId={room.roomId}
|
||||
fileDropContainerRef={containerRef}
|
||||
threadRootId={threadRootId}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</div>
|
||||
{hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />}
|
||||
<RoomInput
|
||||
room={room}
|
||||
editor={editor}
|
||||
roomId={room.roomId}
|
||||
fileDropContainerRef={containerRef}
|
||||
threadRootId={threadRootId}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -802,8 +802,9 @@ export const Message = as<'div', MessageProps>(
|
||||
gap="300"
|
||||
direction={messageLayout === MessageLayout.Compact ? 'RowReverse' : 'Row'}
|
||||
justifyContent="SpaceBetween"
|
||||
alignItems="Baseline"
|
||||
alignItems="Center"
|
||||
grow="Yes"
|
||||
data-message-header=""
|
||||
>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Username
|
||||
|
||||
@@ -55,6 +55,7 @@ export const Reactions = as<'div', ReactionsProps>(
|
||||
className={classNames(css.ReactionsContainer, className)}
|
||||
gap="200"
|
||||
wrap="Wrap"
|
||||
data-reactions=""
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
IThumbnailContent,
|
||||
MATRIX_BLUR_HASH_PROPERTY_NAME,
|
||||
MATRIX_SPOILER_PROPERTY_NAME,
|
||||
PAARROT_CAROUSEL_INDEX_PROPERTY_NAME,
|
||||
PAARROT_CAROUSEL_TOTAL_PROPERTY_NAME,
|
||||
PAARROT_CAROUSEL_UUID_PROPERTY_NAME,
|
||||
} from '../../../types/matrix/common';
|
||||
import {
|
||||
getImageFileUrl,
|
||||
@@ -18,6 +21,12 @@ import { TUploadItem } from '../../state/room/roomInputDrafts';
|
||||
import { encodeBlurHash } from '../../utils/blurHash';
|
||||
import { scaleYDimension } from '../../utils/common';
|
||||
|
||||
type CarouselMetadata = {
|
||||
uuid: string;
|
||||
index: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
const generateThumbnailContent = async (
|
||||
mx: MatrixClient,
|
||||
img: HTMLImageElement | HTMLVideoElement,
|
||||
@@ -46,7 +55,8 @@ const generateThumbnailContent = async (
|
||||
export const getImageMsgContent = async (
|
||||
mx: MatrixClient,
|
||||
item: TUploadItem,
|
||||
mxc: string
|
||||
mxc: string,
|
||||
carouselMetadata?: CarouselMetadata
|
||||
): Promise<IContent> => {
|
||||
const { file, originalFile, encInfo, metadata } = item;
|
||||
const [imgError, imgEl] = await to(loadImageElement(getImageFileUrl(originalFile)));
|
||||
@@ -78,6 +88,11 @@ export const getImageMsgContent = async (
|
||||
} else {
|
||||
content.url = mxc;
|
||||
}
|
||||
if (carouselMetadata) {
|
||||
content[PAARROT_CAROUSEL_UUID_PROPERTY_NAME] = carouselMetadata.uuid;
|
||||
content[PAARROT_CAROUSEL_INDEX_PROPERTY_NAME] = carouselMetadata.index;
|
||||
content[PAARROT_CAROUSEL_TOTAL_PROPERTY_NAME] = carouselMetadata.total;
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user