feat(composer): dock the emoji and sticker picker inline at the top of the composer on native
This commit is contained in:
parent
aab65b573a
commit
18eddec405
4 changed files with 121 additions and 86 deletions
|
|
@ -371,6 +371,7 @@ type EmojiBoardProps = {
|
||||||
onStickerSelect?: (mxc: string, shortcode: string, label: string) => void;
|
onStickerSelect?: (mxc: string, shortcode: string, label: string) => void;
|
||||||
allowTextCustomEmoji?: boolean;
|
allowTextCustomEmoji?: boolean;
|
||||||
addToRecentEmoji?: boolean;
|
addToRecentEmoji?: boolean;
|
||||||
|
dock?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function EmojiBoard({
|
export function EmojiBoard({
|
||||||
|
|
@ -384,6 +385,7 @@ export function EmojiBoard({
|
||||||
onStickerSelect,
|
onStickerSelect,
|
||||||
allowTextCustomEmoji,
|
allowTextCustomEmoji,
|
||||||
addToRecentEmoji = true,
|
addToRecentEmoji = true,
|
||||||
|
dock,
|
||||||
}: EmojiBoardProps) {
|
}: EmojiBoardProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -513,6 +515,7 @@ export function EmojiBoard({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EmojiBoardLayout
|
<EmojiBoardLayout
|
||||||
|
dock={dock}
|
||||||
header={
|
header={
|
||||||
<Box direction="Column" gap="200">
|
<Box direction="Column" gap="200">
|
||||||
{onTabChange && <EmojiBoardTabs tab={tab} onTabChange={onTabChange} />}
|
{onTabChange && <EmojiBoardTabs tab={tab} onTabChange={onTabChange} />}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,12 @@ export const EmojiBoardLayout = as<
|
||||||
header: ReactNode;
|
header: ReactNode;
|
||||||
sidebar?: ReactNode;
|
sidebar?: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
dock?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, header, sidebar, children, ...props }, ref) => (
|
>(({ className, header, sidebar, children, dock, ...props }, ref) => (
|
||||||
<Box
|
<Box
|
||||||
display="InlineFlex"
|
display={dock ? 'Flex' : 'InlineFlex'}
|
||||||
className={classNames(css.Base, className)}
|
className={classNames(css.Base, dock && css.BaseDock, className)}
|
||||||
direction="Row"
|
direction="Row"
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,21 @@ export const Base = style({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Docked variant — the board lives inline at the top of the composer on native
|
||||||
|
// instead of floating as a pop-out. Full-width, capped height, no shadow/round
|
||||||
|
// corners (the composer pill already clips it); only a hairline parts it from
|
||||||
|
// the textarea row below.
|
||||||
|
export const BaseDock = style({
|
||||||
|
maxWidth: 'unset',
|
||||||
|
width: '100%',
|
||||||
|
height: toRem(320),
|
||||||
|
maxHeight: '46vh',
|
||||||
|
border: 'none',
|
||||||
|
borderBottom: `${config.borderWidth.B300} solid rgba(255, 255, 255, 0.08)`,
|
||||||
|
borderRadius: 0,
|
||||||
|
boxShadow: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
export const Header = style({
|
export const Header = style({
|
||||||
padding: config.space.S300,
|
padding: config.space.S300,
|
||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ import {
|
||||||
getMentions,
|
getMentions,
|
||||||
} from '../../components/editor';
|
} from '../../components/editor';
|
||||||
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
import {
|
import {
|
||||||
TUploadContent,
|
TUploadContent,
|
||||||
encryptFile,
|
encryptFile,
|
||||||
|
|
@ -216,6 +216,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
const isOneOnOne = useIsOneOnOne();
|
const isOneOnOne = useIsOneOnOne();
|
||||||
const commands = useCommands(mx, room);
|
const commands = useCommands(mx, room);
|
||||||
const emojiBtnRef = useRef<HTMLButtonElement>(null);
|
const emojiBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const screenSize = useScreenSizeContext();
|
||||||
|
// On native / narrow screens the emoji-sticker board is docked inline at the
|
||||||
|
// top of the composer instead of floating as a pop-out.
|
||||||
|
const dockEmojiBoard = mobileOrTablet() || screenSize === ScreenSize.Mobile;
|
||||||
|
const [emojiBoardTab, setEmojiBoardTab] = useState<EmojiBoardTab | undefined>(undefined);
|
||||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||||
const powerLevels = usePowerLevelsContext();
|
const powerLevels = usePowerLevelsContext();
|
||||||
const creators = useRoomCreators(room);
|
const creators = useRoomCreators(room);
|
||||||
|
|
@ -601,54 +606,62 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
const emojiButton = (
|
const closeEmojiBoard = () => {
|
||||||
<UseStateProvider initial={undefined}>
|
setEmojiBoardTab(undefined);
|
||||||
{(emojiBoardTab: EmojiBoardTab | undefined, setEmojiBoardTab) => (
|
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
||||||
<PopOut
|
};
|
||||||
offset={16}
|
|
||||||
alignOffset={-44}
|
const emojiBoard = (
|
||||||
position="Top"
|
<EmojiBoard
|
||||||
align="End"
|
tab={emojiBoardTab ?? EmojiBoardTab.Emoji}
|
||||||
anchor={
|
onTabChange={setEmojiBoardTab}
|
||||||
emojiBoardTab === undefined
|
imagePackRooms={imagePackRooms}
|
||||||
? undefined
|
returnFocusOnDeactivate={false}
|
||||||
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
dock={dockEmojiBoard}
|
||||||
}
|
onEmojiSelect={handleEmoticonSelect}
|
||||||
content={
|
onCustomEmojiSelect={handleEmoticonSelect}
|
||||||
<EmojiBoard
|
onStickerSelect={handleStickerSelect}
|
||||||
tab={emojiBoardTab ?? EmojiBoardTab.Emoji}
|
requestClose={closeEmojiBoard}
|
||||||
onTabChange={setEmojiBoardTab}
|
/>
|
||||||
imagePackRooms={imagePackRooms}
|
);
|
||||||
returnFocusOnDeactivate={false}
|
|
||||||
onEmojiSelect={handleEmoticonSelect}
|
const emojiTriggerButton = (
|
||||||
onCustomEmojiSelect={handleEmoticonSelect}
|
<IconButton
|
||||||
onStickerSelect={handleStickerSelect}
|
ref={emojiBtnRef}
|
||||||
requestClose={() => {
|
aria-pressed={!!emojiBoardTab}
|
||||||
setEmojiBoardTab((tab) => {
|
onClick={() =>
|
||||||
if (tab) {
|
dockEmojiBoard
|
||||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
? setEmojiBoardTab(emojiBoardTab ? undefined : EmojiBoardTab.Emoji)
|
||||||
return undefined;
|
: setEmojiBoardTab(EmojiBoardTab.Emoji)
|
||||||
}
|
}
|
||||||
return tab;
|
variant="SurfaceVariant"
|
||||||
});
|
fill="None"
|
||||||
}}
|
size="300"
|
||||||
/>
|
radii="300"
|
||||||
}
|
>
|
||||||
>
|
<Icon src={StreamComposerIcons.Smile} />
|
||||||
<IconButton
|
</IconButton>
|
||||||
ref={emojiBtnRef}
|
);
|
||||||
aria-pressed={!!emojiBoardTab}
|
|
||||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
|
// Native docks the board inline (rendered in the composer's top slot); the
|
||||||
variant="SurfaceVariant"
|
// desktop trigger keeps the floating pop-out anchored to the button.
|
||||||
fill="None"
|
const emojiButton = dockEmojiBoard ? (
|
||||||
size="300"
|
emojiTriggerButton
|
||||||
radii="300"
|
) : (
|
||||||
>
|
<PopOut
|
||||||
<Icon src={StreamComposerIcons.Smile} />
|
offset={16}
|
||||||
</IconButton>
|
alignOffset={-44}
|
||||||
</PopOut>
|
position="Top"
|
||||||
)}
|
align="End"
|
||||||
</UseStateProvider>
|
anchor={
|
||||||
|
emojiBoardTab === undefined
|
||||||
|
? undefined
|
||||||
|
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
||||||
|
}
|
||||||
|
content={emojiBoard}
|
||||||
|
>
|
||||||
|
{emojiTriggerButton}
|
||||||
|
</PopOut>
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendButton = (
|
const sendButton = (
|
||||||
|
|
@ -785,43 +798,46 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
top={
|
top={
|
||||||
replyDraft && (
|
<>
|
||||||
<div>
|
{dockEmojiBoard && emojiBoardTab !== undefined && emojiBoard}
|
||||||
<Box
|
{replyDraft && (
|
||||||
alignItems="Center"
|
<div>
|
||||||
gap="300"
|
<Box
|
||||||
style={{ padding: `${config.space.S200} ${config.space.S300} 0` }}
|
alignItems="Center"
|
||||||
>
|
gap="300"
|
||||||
<IconButton
|
style={{ padding: `${config.space.S200} ${config.space.S300} 0` }}
|
||||||
onClick={() => setReplyDraft(undefined)}
|
|
||||||
variant="SurfaceVariant"
|
|
||||||
size="300"
|
|
||||||
radii="300"
|
|
||||||
>
|
>
|
||||||
<Icon src={Icons.Cross} size="50" />
|
<IconButton
|
||||||
</IconButton>
|
onClick={() => setReplyDraft(undefined)}
|
||||||
<Box direction="Row" gap="200" alignItems="Center">
|
variant="SurfaceVariant"
|
||||||
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
|
size="300"
|
||||||
<ReplyLayout
|
radii="300"
|
||||||
userColor={replyUsernameColor}
|
|
||||||
username={
|
|
||||||
<Text size="T300" truncate>
|
|
||||||
<b>
|
|
||||||
{getMemberDisplayName(room, replyDraft.userId) ??
|
|
||||||
getMxIdLocalPart(replyDraft.userId) ??
|
|
||||||
replyDraft.userId}
|
|
||||||
</b>
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Text size="T300" truncate>
|
<Icon src={Icons.Cross} size="50" />
|
||||||
{trimReplyFromBody(replyDraft.body)}
|
</IconButton>
|
||||||
</Text>
|
<Box direction="Row" gap="200" alignItems="Center">
|
||||||
</ReplyLayout>
|
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
|
||||||
|
<ReplyLayout
|
||||||
|
userColor={replyUsernameColor}
|
||||||
|
username={
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
<b>
|
||||||
|
{getMemberDisplayName(room, replyDraft.userId) ??
|
||||||
|
getMxIdLocalPart(replyDraft.userId) ??
|
||||||
|
replyDraft.userId}
|
||||||
|
</b>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
{trimReplyFromBody(replyDraft.body)}
|
||||||
|
</Text>
|
||||||
|
</ReplyLayout>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)
|
</>
|
||||||
}
|
}
|
||||||
bottom={
|
bottom={
|
||||||
<Box
|
<Box
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue