style(rooms): redesign the join-before-navigate screen as a full-width Dawn invite hero with accept and decline
This commit is contained in:
parent
15ce5f4fb9
commit
08456b63ad
2 changed files with 210 additions and 26 deletions
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { color, config, toRem } from 'folds';
|
||||||
|
|
||||||
|
// A calm centred invite hero: full-width on mobile, a narrow reading column on
|
||||||
|
// desktop. Replaces the cramped 364px RoomCard that floated in an empty page.
|
||||||
|
export const Hero = style({
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: toRem(440),
|
||||||
|
margin: 'auto',
|
||||||
|
padding: `${config.space.S700} ${config.space.S400}`,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
gap: config.space.S400,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Eyebrow = style({
|
||||||
|
display: 'block',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
fontSize: toRem(11),
|
||||||
|
fontWeight: config.fontWeight.W600,
|
||||||
|
color: color.Surface.OnContainer,
|
||||||
|
opacity: 0.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Topic = style({
|
||||||
|
color: color.SurfaceVariant.OnContainer,
|
||||||
|
opacity: 0.8,
|
||||||
|
maxWidth: toRem(380),
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 5,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MemberCount = style({
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontVariantNumeric: 'tabular-nums',
|
||||||
|
color: color.SurfaceVariant.OnContainer,
|
||||||
|
opacity: 0.7,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Full-width action stack — comfortable touch targets on native.
|
||||||
|
export const Actions = style({
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: config.space.S200,
|
||||||
|
marginTop: config.space.S300,
|
||||||
|
});
|
||||||
|
|
@ -1,15 +1,38 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Box, Icon, IconButton, Icons, Scroll, Text, toRem } from 'folds';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Icons,
|
||||||
|
Scroll,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
color,
|
||||||
|
toRem,
|
||||||
|
} from 'folds';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { RoomCard } from '../../components/room-card';
|
import { MatrixError, Room } from 'matrix-js-sdk';
|
||||||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import * as css from './JoinBeforeNavigate.css';
|
||||||
|
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||||
import { Page, PageHeader } from '../../components/page';
|
import { Page, PageHeader } from '../../components/page';
|
||||||
import { RoomSummaryLoader } from '../../components/RoomSummaryLoader';
|
import { RoomSummaryLoader } from '../../components/RoomSummaryLoader';
|
||||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
|
import { useJoinedRoomId } from '../../hooks/useJoinedRoomId';
|
||||||
import { allRoomsAtom } from '../../state/room-list/roomList';
|
import { allRoomsAtom } from '../../state/room-list/roomList';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
||||||
|
import { getRoomAvatarUrl } from '../../utils/room';
|
||||||
|
import { RoomType } from '../../../types/matrix/room';
|
||||||
|
import { millify } from '../../plugins/millify';
|
||||||
|
|
||||||
type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
|
type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
|
||||||
export function JoinBeforeNavigate({
|
export function JoinBeforeNavigate({
|
||||||
|
|
@ -17,11 +40,20 @@ export function JoinBeforeNavigate({
|
||||||
eventId,
|
eventId,
|
||||||
viaServers,
|
viaServers,
|
||||||
}: JoinBeforeNavigateProps) {
|
}: JoinBeforeNavigateProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
const useAuthentication = useMediaAuthentication();
|
||||||
const allRooms = useAtomValue(allRoomsAtom);
|
const allRooms = useAtomValue(allRoomsAtom);
|
||||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||||
|
const navigate = useNavigate();
|
||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
|
|
||||||
|
const joinedRoomId = useJoinedRoomId(allRooms, roomIdOrAlias);
|
||||||
|
// For a pending invite the room already exists locally (membership "invite"),
|
||||||
|
// so resolve it for the richer avatar / member count / membership signal.
|
||||||
|
const room = mx.getRoom(joinedRoomId ?? roomIdOrAlias) ?? undefined;
|
||||||
|
const invited = room?.getMyMembership() === 'invite';
|
||||||
|
|
||||||
const handleView = (roomId: string) => {
|
const handleView = (roomId: string) => {
|
||||||
if (mx.getRoom(roomId)?.isSpaceRoom()) {
|
if (mx.getRoom(roomId)?.isSpaceRoom()) {
|
||||||
navigateSpace(roomId);
|
navigateSpace(roomId);
|
||||||
|
|
@ -30,6 +62,21 @@ export function JoinBeforeNavigate({
|
||||||
navigateRoom(roomId, eventId);
|
navigateRoom(roomId, eventId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
|
||||||
|
useCallback(() => mx.joinRoom(roomIdOrAlias, { viaServers }), [mx, roomIdOrAlias, viaServers])
|
||||||
|
);
|
||||||
|
const joining =
|
||||||
|
joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;
|
||||||
|
|
||||||
|
const [declineState, decline] = useAsyncCallback<void, MatrixError, []>(
|
||||||
|
useCallback(async () => {
|
||||||
|
if (room) await mx.leave(room.roomId);
|
||||||
|
navigate('/');
|
||||||
|
}, [mx, room, navigate])
|
||||||
|
);
|
||||||
|
const declining =
|
||||||
|
declineState.status === AsyncStatus.Loading || declineState.status === AsyncStatus.Success;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader balance>
|
<PageHeader balance>
|
||||||
|
|
@ -46,35 +93,119 @@ export function JoinBeforeNavigate({
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||||
<Text size="H3" truncate>
|
<Text size="H4" truncate>
|
||||||
{roomIdOrAlias}
|
{room?.name ?? t('Explore.join')}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Scroll hideTrack visibility="Hover" size="0">
|
<Scroll hideTrack visibility="Hover" size="0">
|
||||||
<Box style={{ height: '100%' }} grow="Yes" alignItems="Center" justifyContent="Center">
|
|
||||||
<RoomSummaryLoader roomIdOrAlias={roomIdOrAlias}>
|
<RoomSummaryLoader roomIdOrAlias={roomIdOrAlias}>
|
||||||
{(summary) => (
|
{(summary) => {
|
||||||
<RoomCard
|
const avatarUrl = room
|
||||||
style={{ maxWidth: toRem(364), width: '100%' }}
|
? getRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
: (summary?.avatar_url &&
|
||||||
allRooms={allRooms}
|
mxcUrlToHttp(mx, summary.avatar_url, useAuthentication, 96, 96, 'crop')) ||
|
||||||
avatarUrl={summary?.avatar_url}
|
undefined;
|
||||||
name={summary?.name}
|
const name =
|
||||||
topic={summary?.topic}
|
room?.name ?? summary?.name ?? getMxIdLocalPart(roomIdOrAlias) ?? roomIdOrAlias;
|
||||||
memberCount={summary?.num_joined_members}
|
const topic = summary?.topic;
|
||||||
roomType={summary?.room_type}
|
const memberCount = room?.getJoinedMemberCount() ?? summary?.num_joined_members;
|
||||||
viaServers={viaServers}
|
const isSpace = room?.isSpaceRoom() || summary?.room_type === RoomType.Space;
|
||||||
renderTopicViewer={(name, topic, requestClose) => (
|
const errored = joinState.status === AsyncStatus.Error;
|
||||||
<RoomTopicViewer name={name} topic={topic} requestClose={requestClose} />
|
|
||||||
)}
|
let primaryLabel = t('Explore.join');
|
||||||
onView={handleView}
|
if (joining) primaryLabel = t('Explore.joining');
|
||||||
|
else if (invited) primaryLabel = t('Direct.invite_accept');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={css.Hero}>
|
||||||
|
<Avatar size="500" radii="500" style={{ width: toRem(84), height: toRem(84) }}>
|
||||||
|
<RoomAvatar
|
||||||
|
roomId={room?.roomId ?? roomIdOrAlias}
|
||||||
|
src={avatarUrl ?? undefined}
|
||||||
|
alt={name}
|
||||||
|
renderFallback={() => (
|
||||||
|
<RoomIcon
|
||||||
|
size="600"
|
||||||
|
roomType={isSpace ? RoomType.Space : undefined}
|
||||||
|
filled
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</RoomSummaryLoader>
|
/>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
{invited && <span className={css.Eyebrow}>{t('Room.invite')}</span>}
|
||||||
|
|
||||||
|
<Box direction="Column" gap="200" alignItems="Center">
|
||||||
|
<Text size="H3" align="Center">
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
{isSpace && (
|
||||||
|
<Badge variant="Secondary" fill="Soft" outlined>
|
||||||
|
<Text size="L400">{t('Explore.space_badge')}</Text>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{typeof memberCount === 'number' && (
|
||||||
|
<Box gap="100" alignItems="Center">
|
||||||
|
<Icon size="50" src={Icons.User} />
|
||||||
|
<Text size="T200" as="span" className={css.MemberCount}>
|
||||||
|
{t('Explore.members_count', {
|
||||||
|
count: memberCount,
|
||||||
|
formattedCount: millify(memberCount),
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{topic && (
|
||||||
|
<Text size="T300" className={css.Topic} align="Center">
|
||||||
|
{topic}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={css.Actions}>
|
||||||
|
{typeof joinedRoomId === 'string' ? (
|
||||||
|
<Button onClick={() => handleView(joinedRoomId)} variant="Primary" size="500">
|
||||||
|
<Text size="B500">{t('Explore.view')}</Text>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={join}
|
||||||
|
variant="Primary"
|
||||||
|
size="500"
|
||||||
|
disabled={joining}
|
||||||
|
before={joining && <Spinner size="200" variant="Primary" fill="Solid" />}
|
||||||
|
>
|
||||||
|
<Text size="B500">{primaryLabel}</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{invited && typeof joinedRoomId !== 'string' && (
|
||||||
|
<Button
|
||||||
|
onClick={decline}
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Soft"
|
||||||
|
size="500"
|
||||||
|
disabled={declining}
|
||||||
|
before={declining && <Spinner size="200" variant="Secondary" />}
|
||||||
|
>
|
||||||
|
<Text size="B500">{t('Direct.invite_decline')}</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errored && (
|
||||||
|
<Text size="T200" align="Center" style={{ color: color.Critical.Main }}>
|
||||||
|
{joinState.error.message || t('Explore.join_error_unknown')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</RoomSummaryLoader>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
</Box>
|
</Box>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue