add multi account - init
This commit is contained in:
parent
3409054bc5
commit
91f9f7c399
88 changed files with 1692 additions and 1189 deletions
52
src/app/components/auth-layout/AuthLayout.tsx
Normal file
52
src/app/components/auth-layout/AuthLayout.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { Box, Header, Scroll, Text } from 'folds';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import * as css from './styles.css';
|
||||
import * as PatternsCss from '../../styles/Patterns.css';
|
||||
import CinnySVG from '../../../../public/res/svg/cinny.svg';
|
||||
import { AuthFooter } from './AuthFooter';
|
||||
|
||||
export function AuthLogo({ large }: { large?: boolean }) {
|
||||
return <img className={css.AuthLogo({ large })} src={CinnySVG} alt="Cinny Logo" />;
|
||||
}
|
||||
|
||||
type AuthLayoutProps = {
|
||||
hideHeader?: boolean;
|
||||
center?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
export function AuthLayout({ hideHeader, center, children }: AuthLayoutProps) {
|
||||
return (
|
||||
<Scroll variant="Background" visibility="Hover" size="0" hideTrack>
|
||||
<Box
|
||||
className={classNames(css.AuthLayout, PatternsCss.BackgroundDotPattern)}
|
||||
direction="Column"
|
||||
alignItems="Center"
|
||||
gap="400"
|
||||
>
|
||||
<Box
|
||||
style={{ width: '100%' }}
|
||||
justifyContent={center ? 'Center' : undefined}
|
||||
alignItems="Center"
|
||||
direction="Column"
|
||||
>
|
||||
<Box direction="Column" className={css.AuthCard}>
|
||||
{!hideHeader && (
|
||||
<Header className={css.AuthHeader} size="600" variant="Surface">
|
||||
<Box grow="Yes" direction="Row" gap="300" alignItems="Center">
|
||||
<AuthLogo />
|
||||
<Text size="H3">Cinny</Text>
|
||||
</Box>
|
||||
</Header>
|
||||
)}
|
||||
<Box className={css.AuthCardContent} direction="Column">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<AuthFooter />
|
||||
</Box>
|
||||
</Scroll>
|
||||
);
|
||||
}
|
||||
1
src/app/components/auth-layout/index.ts
Normal file
1
src/app/components/auth-layout/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './AuthLayout';
|
||||
63
src/app/components/auth-layout/styles.css.ts
Normal file
63
src/app/components/auth-layout/styles.css.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { recipe } from '@vanilla-extract/recipes';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
|
||||
export const AuthLayout = style({
|
||||
minHeight: '100%',
|
||||
backgroundColor: color.Background.Container,
|
||||
color: color.Background.OnContainer,
|
||||
padding: config.space.S400,
|
||||
paddingBottom: 0,
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const AuthCard = style({
|
||||
marginTop: '1vh',
|
||||
maxWidth: toRem(460),
|
||||
width: '100%',
|
||||
backgroundColor: color.Surface.Container,
|
||||
color: color.Surface.OnContainer,
|
||||
borderRadius: config.radii.R500,
|
||||
boxShadow: config.shadow.E100,
|
||||
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const AuthLogo = recipe({
|
||||
base: [
|
||||
DefaultReset,
|
||||
{
|
||||
width: toRem(26),
|
||||
height: toRem(26),
|
||||
|
||||
borderRadius: '50%',
|
||||
},
|
||||
],
|
||||
variants: {
|
||||
large: {
|
||||
true: {
|
||||
width: toRem(48),
|
||||
height: toRem(48),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const AuthHeader = style({
|
||||
padding: `0 ${config.space.S400}`,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
});
|
||||
|
||||
export const AuthCardContent = style({
|
||||
maxWidth: toRem(402),
|
||||
width: '100%',
|
||||
margin: 'auto',
|
||||
padding: config.space.S400,
|
||||
paddingTop: config.space.S700,
|
||||
paddingBottom: toRem(44),
|
||||
gap: toRem(44),
|
||||
});
|
||||
|
||||
export const AuthFooter = style({
|
||||
padding: config.space.S200,
|
||||
});
|
||||
|
|
@ -34,7 +34,6 @@ import { HeadingLevel } from './slate';
|
|||
import { isMacOS } from '../../utils/user-agent';
|
||||
import { KeySymbol } from '../../utils/key-symbol';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
|
||||
function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) {
|
||||
|
|
@ -260,7 +259,7 @@ export function Toolbar() {
|
|||
const canEscape = isBlockActive(editor, BlockType.Paragraph)
|
||||
? isAnyMarkActive(editor)
|
||||
: ReactEditor.isFocused(editor);
|
||||
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||
const [isMarkdown, setIsMarkdown] = useSetting('isMarkdown');
|
||||
|
||||
return (
|
||||
<Box className={css.EditorToolbarBase}>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { RoomAvatar } from '../room-avatar';
|
|||
import { nameInitials } from '../../utils/common';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { InviteUserPrompt } from '../invite-user-prompt';
|
||||
import { useClient } from '../../hooks/useClient';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
|
|
@ -51,7 +50,7 @@ export const RoomIntro = as<
|
|||
'room_id' in event.content.predecessor &&
|
||||
event.content.predecessor.room_id;
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
|
||||
return (
|
||||
<Box direction="Column" grow="Yes" gap="500" {...props} ref={ref}>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import * as css from './styles.css';
|
|||
import { PickerColumn } from './PickerColumn';
|
||||
import { hour12to24, hour24to12, hoursToMs, inSameDay, minutesToMs } from '../../utils/time';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
|
||||
type TimePickerProps = {
|
||||
min: number;
|
||||
|
|
@ -15,7 +14,7 @@ type TimePickerProps = {
|
|||
};
|
||||
export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
||||
({ min, max, value, onChange }, ref) => {
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
|
||||
const hour24 = dayjs(value).hour();
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import { ImageViewer } from '../image-viewer';
|
|||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { LINKIFY_OPTS } from '../../plugins/react-custom-html-parser';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { UserBiography, UserPronoun } from '../../hooks/useUserProfile';
|
||||
import { RoomTopicViewer } from '../room-topic-viewer';
|
||||
|
||||
|
|
@ -141,7 +140,7 @@ export function UserHeroAvatar({ userId, avatarUrl, presence }: UserHeroAvatarPr
|
|||
}
|
||||
|
||||
export function UserTimezone({ timezone }: { timezone: string }) {
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
|
||||
let time: string | undefined;
|
||||
let offset: string | undefined;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import { SettingTile } from '../setting-tile';
|
|||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { BreakWord } from '../../styles/Text.css';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { timeDayMonYear, timeHourMinute } from '../../utils/time';
|
||||
import { useClient } from '../../hooks/useClient';
|
||||
import { getIgnoredUsersContent, useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
|
|
@ -41,8 +40,8 @@ type UserKickAlertProps = {
|
|||
ts?: number;
|
||||
};
|
||||
export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
const [dateFormatString] = useSetting('dateFormatString');
|
||||
|
||||
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
|
||||
|
|
@ -91,8 +90,8 @@ type UserBanAlertProps = {
|
|||
export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBanAlertProps) {
|
||||
const ct = useClient();
|
||||
const roomId = useRoom();
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
const [dateFormatString] = useSetting('dateFormatString');
|
||||
|
||||
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
|
||||
|
|
@ -176,8 +175,8 @@ type UserInviteAlertProps = {
|
|||
export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: UserInviteAlertProps) {
|
||||
const ct = useClient();
|
||||
const roomId = useRoom();
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
const [dateFormatString] = useSetting('dateFormatString');
|
||||
|
||||
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
|
||||
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import { SequenceCard } from '../../../components/sequence-card';
|
|||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { StateEventEditor } from './StateEventEditor';
|
||||
|
|
@ -221,7 +220,7 @@ type DeveloperToolsProps = {
|
|||
requestClose: () => void;
|
||||
};
|
||||
export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
||||
const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||
const [developerTools, setDeveloperTools] = useSetting('developerTools');
|
||||
const ct = useClient();
|
||||
const roomId = useRoom();
|
||||
|
||||
|
|
@ -255,8 +254,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
|||
if (openAccountData !== undefined) {
|
||||
return (
|
||||
<AccountDataEditor
|
||||
type={openAccountData?.type}
|
||||
content={openAccountData?.content}
|
||||
event={openAccountData ?? undefined}
|
||||
submitChange={submitAccountData}
|
||||
requestClose={handleClose}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import { useDebounce } from '../../../hooks/useDebounce';
|
|||
import { useAsyncSearch, UseAsyncSearchOptions } from '../../../hooks/useAsyncSearch';
|
||||
import { useMembershipFilter, useMembershipFilterMenu } from '../../../hooks/useMemberFilter';
|
||||
import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../../hooks/useMemberSort';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||
import { MembershipFilterMenu } from '../../../components/MembershipFilterMenu';
|
||||
|
|
@ -90,7 +89,7 @@ export function Members({ requestClose }: MembersProps) {
|
|||
|
||||
const membershipFilterMenu = useMembershipFilterMenu(membersAPI.getMembership);
|
||||
const sortFilterMenu = useMemberSortMenu(membersAPI.getName);
|
||||
const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex');
|
||||
const [sortFilterIndex, setSortFilterIndex] = useSetting('memberSortFilterIndex');
|
||||
const [membershipFilterIndex, setMembershipFilterIndex] = useState(0);
|
||||
|
||||
const membershipFilter = useMembershipFilter(membershipFilterIndex, membershipFilterMenu);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import { spaceRoomsAtom } from '../../state/spaceRooms';
|
|||
import { MembersDrawer } from '../room/MembersDrawer';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { LobbyHeader } from './LobbyHeader';
|
||||
import { LobbyHero } from './LobbyHero';
|
||||
import { ScrollTopContainer } from '../../components/scroll-top-container';
|
||||
|
|
@ -174,7 +173,7 @@ export function Lobby() {
|
|||
const heroSectionRef = useRef<HTMLDivElement>(null);
|
||||
const [heroSectionHeight, setHeroSectionHeight] = useState<number>();
|
||||
const setSpaceRooms = useSetAtom(spaceRoomsAtom);
|
||||
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [isDrawer] = useSetting('isPeopleDrawer');
|
||||
const screenSize = useScreenSizeContext();
|
||||
const [onTop, setOnTop] = useState(true);
|
||||
const [closedCategories, setClosedCategories] = useAtom(useClosedLobbyCategoriesAtom());
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import {
|
|||
import FocusTrap from 'focus-trap-react';
|
||||
import { PageHeader } from '../../components/page';
|
||||
import { useSetSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSpace } from '../../hooks/useSpace';
|
||||
import { RoomAvatar } from '../../components/room-avatar';
|
||||
import { nameInitials } from '../../utils/common';
|
||||
|
|
@ -145,7 +144,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||
const ct = useClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const spaceId = useSpace();
|
||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const setPeopleDrawer = useSetSetting('isPeopleDrawer');
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ import { SearchOrderBy } from 'matrix-js-sdk';
|
|||
import { PageHero, PageHeroEmpty, PageHeroSection } from '../../components/page';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { _SearchPathSearchParams } from '../../pages/paths';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSettings } from '../../state/hooks/settings';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
import { ScrollTopContainer } from '../../components/scroll-top-container';
|
||||
|
|
@ -53,12 +52,8 @@ export function MessageSearch({
|
|||
const mx = useMatrixClient();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const allRooms = useRooms(mx, allRoomsAtom, mDirects);
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const { mediaAutoLoad, urlPreview, legacyUsernameColor, hour24Clock, dateFormatString } =
|
||||
useSettings();
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import { getMatrixToRoom } from '../../plugins/matrix-to';
|
|||
import { getViaServers } from '../../plugins/via-servers';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
|
||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import {
|
||||
|
|
@ -61,7 +60,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
|||
({ roomId, requestClose, notificationMode, notifications }, ref) => {
|
||||
const ct = useClient();
|
||||
const { userId } = ct.session;
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
const powerLevelState = usePowerLevelsState(roomId);
|
||||
const powerLevels = usePowerLevels(powerLevelState.value);
|
||||
const creators = useRoomCreators(roomId);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import { useDebounce } from '../../hooks/useDebounce';
|
|||
import { TypingIndicator } from '../../components/typing-indicator';
|
||||
import { getMemberSearchStr } from '../../utils/room';
|
||||
import { useSetSetting, useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { millify } from '../../plugins/millify';
|
||||
import { ScrollTopContainer } from '../../components/scroll-top-container';
|
||||
import { UserAvatar } from '../../components/user-avatar';
|
||||
|
|
@ -69,7 +68,7 @@ type MemberDrawerHeaderProps = {
|
|||
memberCount: number;
|
||||
};
|
||||
function MemberDrawerHeader({ memberCount }: MemberDrawerHeaderProps) {
|
||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const setPeopleDrawer = useSetSetting('isPeopleDrawer');
|
||||
|
||||
return (
|
||||
<Header className={css.MembersDrawerHeader} variant="Background" size="600">
|
||||
|
|
@ -216,7 +215,7 @@ export function MembersDrawer() {
|
|||
|
||||
const membershipFilterMenu = useMembershipFilterMenu(membersAPI.getMembership);
|
||||
const sortFilterMenu = useMemberSortMenu(membersAPI.getName);
|
||||
const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex');
|
||||
const [sortFilterIndex, setSortFilterIndex] = useSetting('memberSortFilterIndex');
|
||||
const [membershipFilterIndex, setMembershipFilterIndex] = useState(0);
|
||||
|
||||
const membershipFilter = useMembershipFilter(membershipFilterIndex, membershipFilterMenu);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { RoomView } from './RoomView';
|
|||
import { MembersDrawer } from './MembersDrawer';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import {
|
||||
PowerLevelsContextProvider,
|
||||
usePowerLevels,
|
||||
|
|
@ -39,8 +38,8 @@ export function Room() {
|
|||
const threadTimeline = activeThreadId ? threads?.get(activeThreadId) : undefined;
|
||||
const threadFocusRef = useRef<boolean>(false);
|
||||
|
||||
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [isDrawer] = useSetting('isPeopleDrawer');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
const screenSize = useScreenSizeContext();
|
||||
const powerLevelStateResult = usePowerLevelsState(roomId);
|
||||
const powerLevels = usePowerLevels(powerLevelStateResult.value);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import {
|
|||
UploadBoardImperativeHandlers,
|
||||
} from '../../components/upload-board';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { CommandAutocomplete } from './CommandAutocomplete';
|
||||
import { useCommands } from '../../hooks/useCommands';
|
||||
import { mobileOrTablet } from '../../utils/user-agent';
|
||||
|
|
@ -81,9 +80,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
({ editor, fileDropContainerRef, threadId }, ref) => {
|
||||
const roomId = useRoom();
|
||||
|
||||
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const [enterForNewline] = useSetting('enterForNewline');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
const [legacyUsernameColor] = useSetting('legacyUsernameColor');
|
||||
const [ping, setPing] = useState(true);
|
||||
|
||||
const direct = useIsDirectRoom();
|
||||
|
|
@ -129,7 +128,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
|
||||
const imagePackRooms: string[] = useImagePackRooms(roomId);
|
||||
|
||||
const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
||||
const [toolbar, setToolbar] = useSetting('editorToolbar');
|
||||
const [autocompleteQuery, setAutocompleteQuery] =
|
||||
useState<AutocompleteQuery<AutocompletePrefix>>();
|
||||
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import {
|
|||
RedactedContent,
|
||||
} from '../../components/message';
|
||||
import { getLatestEditEvent, getReplyId, getThreadId } from '../../utils/room';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { MessageLayout, settingsAtom } from '../../state/settings';
|
||||
import { useSettings } from '../../state/hooks/settings';
|
||||
import { MessageLayout } from '../../state/settings';
|
||||
import { Reactions, Message, ThreadEvents } from './message';
|
||||
import { RoomIntro } from '../../components/room-intro';
|
||||
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||
|
|
@ -89,20 +89,22 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
|
|||
const roomId = useRoom();
|
||||
const encryptedRooms = useObservableValue(ct.encryptedRooms);
|
||||
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
|
||||
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const direct = useIsDirectRoom();
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [encUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
|
||||
const showUrlPreview = encryptedRooms.has(roomId) ? encUrlPreview : urlPreview;
|
||||
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
|
||||
const [showDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||
const {
|
||||
hideActivity,
|
||||
messageLayout,
|
||||
messageSpacing,
|
||||
legacyUsernameColor,
|
||||
mediaAutoLoad,
|
||||
urlPreview,
|
||||
encUrlPreview,
|
||||
showHiddenEvents,
|
||||
developerTools,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
} = useSettings();
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const direct = useIsDirectRoom();
|
||||
const showUrlPreview = encryptedRooms.has(roomId) ? encUrlPreview : urlPreview;
|
||||
|
||||
const ignoredUsersList = useIgnoredUsers();
|
||||
const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]);
|
||||
|
|
@ -415,7 +417,7 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
|
|||
onEditId={handleEdit}
|
||||
redacted={redacted.has(event.event_id)}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
showDeveloperTools={developerTools}
|
||||
memberPowerTag={getMemberPowerTag(event.sender)}
|
||||
accessibleTagColors={accessiblePowerTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { RoomTombstone } from './RoomTombstone';
|
|||
import { RoomInput } from './RoomInput';
|
||||
import { RoomViewFollowing } from './RoomViewFollowing';
|
||||
import { Page } from '../../components/page';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
|
|
@ -29,7 +28,7 @@ export function RoomView({ eventId, timeline, threadFocusRef }: RoomViewProps) {
|
|||
const roomInputRef = useRef<HTMLDivElement>(null);
|
||||
const roomViewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
|
||||
const roomId = useRoom();
|
||||
const editor = useEditor();
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import { UseStateProvider } from '../../components/UseStateProvider';
|
|||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
|
||||
import { _SearchPathSearchParams } from '../../pages/paths';
|
||||
|
|
@ -78,7 +77,7 @@ type RoomMenuProps = {
|
|||
const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ requestClose }, ref) => {
|
||||
const ct = useClient();
|
||||
const roomId = useRoom();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
|
||||
const roomNotifications = useRoomNotifications(roomId, roomToNotificationsAtom);
|
||||
const notifications = useRoomTotalNotifications(roomNotifications);
|
||||
|
|
@ -287,7 +286,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||
})
|
||||
: undefined;
|
||||
|
||||
const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [peopleDrawer, setPeopleDrawer] = useSetting('isPeopleDrawer');
|
||||
|
||||
const handleSearchClick = () => {
|
||||
const searchParams: _SearchPathSearchParams = {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
trimCommand,
|
||||
trimCustomHtml,
|
||||
} from '../../../components/editor';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { MessageEvent } from '../../../../types/matrix/room';
|
||||
import { Command, CommandRecord, SHRUG, TABLEFLIP, UNFLIP } from '../../../hooks/useCommands';
|
||||
|
|
@ -88,7 +87,7 @@ export const useSendMessage = (
|
|||
): SendMessageCallback => {
|
||||
const ct = useClient();
|
||||
const roomId = useRoom();
|
||||
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||
const [isMarkdown] = useSetting('isMarkdown');
|
||||
|
||||
const sendLocalMessage = useSendLocalMessage();
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import { useRoom } from '../../../hooks/useRoom';
|
|||
import { getToday, getYesterday, timeDayMonthYear, timeHourMinute } from '../../../utils/time';
|
||||
import { DatePicker, TimePicker } from '../../../components/time-date';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useClient } from '../../../hooks/useClient';
|
||||
|
||||
type JumpToTimeProps = {
|
||||
|
|
@ -46,7 +45,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
|
|||
ct.state.join.create.read.find((e) => e.roomId === roomId)?.event.origin_server_ts ?? 0;
|
||||
const [ts, setTs] = useState(() => Date.now());
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
|
||||
const [timePickerCords, setTimePickerCords] = useState<RectCords>();
|
||||
const [datePickerCords, setDatePickerCords] = useState<RectCords>();
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import {
|
|||
getMentions,
|
||||
} from '../../../components/editor';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||
import { EmojiBoard } from '../../../components/emoji-board';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
|
|
@ -66,9 +65,9 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
|||
({ roomId, event, imagePackRooms, onCancel, ...props }, ref) => {
|
||||
const ct = useClient();
|
||||
const editor = useEditor();
|
||||
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
const [globalToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
||||
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||
const [enterForNewline] = useSetting('enterForNewline');
|
||||
const [globalToolbar] = useSetting('editorToolbar');
|
||||
const [isMarkdown] = useSetting('isMarkdown');
|
||||
const [toolbar, setToolbar] = useState(globalToolbar);
|
||||
const isComposing = useComposingCheck();
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ import {
|
|||
import { RenderMatrixEvent, useMatrixEventRenderer } from '../../../hooks/useMatrixEventRenderer';
|
||||
import { RenderMessageContent } from '../../../components/RenderMessageContent';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import * as customHtmlCss from '../../../styles/CustomHtml.css';
|
||||
import { EncryptedContent } from '../message';
|
||||
import { Image } from '../../../components/media';
|
||||
|
|
@ -272,14 +271,14 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(({ reque
|
|||
const pinnedEvents = useRoomPinnedEvents(roomId);
|
||||
const sortedPinnedEvent = useMemo(() => Array.from(pinnedEvents).reverse(), [pinnedEvents]);
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [mediaAutoLoad] = useSetting('mediaAutoLoad');
|
||||
const [urlPreview] = useSetting('urlPreview');
|
||||
|
||||
const direct = useIsDirectRoom();
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const [legacyUsernameColor] = useSetting('legacyUsernameColor');
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
const [dateFormatString] = useSetting('dateFormatString');
|
||||
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { useEditor } from '../../../components/editor';
|
|||
import { RoomInput } from '../RoomInput';
|
||||
import { TimelineView } from './TimelineView';
|
||||
import { useEditorAutoFocus, useThreadFocus } from '../hooks/useEditorAutoFocus';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { FollowingMembersPlaceholder } from '../components';
|
||||
import { ThreadViewFollowing } from './ThreadViewFollowing';
|
||||
|
|
@ -27,7 +26,7 @@ export function ThreadView({ threadTimeline, threadFocusRef, requestClose }: Thr
|
|||
const editor = useEditor();
|
||||
const threadInputRef = useRef<HTMLDivElement>(null);
|
||||
const threadViewRef = useRef<HTMLDivElement>(null);
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
|
||||
useThreadFocus(threadTimeline.threadId, threadFocusRef, threadViewRef);
|
||||
useEditorAutoFocus(editor, threadFocusRef, true);
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import {
|
|||
RedactedContent,
|
||||
} from '../../../components/message';
|
||||
import { fallbackReply, getLatestEditEvent, getReplyId } from '../../../utils/room';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { MessageLayout, settingsAtom } from '../../../state/settings';
|
||||
import { useSettings } from '../../../state/hooks/settings';
|
||||
import { MessageLayout } from '../../../state/settings';
|
||||
import { Reactions, Message } from '../message';
|
||||
import * as css from './styles.css';
|
||||
import { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
|
||||
|
|
@ -86,19 +86,21 @@ export function TimelineView({ threadTimeline, threadInputRef }: TimelineViewPro
|
|||
const encryptedRooms = useObservableValue(ct.encryptedRooms);
|
||||
const editor = useEditor();
|
||||
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
|
||||
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const direct = useIsDirectRoom();
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [encUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
|
||||
const showUrlPreview = encryptedRooms.has(roomId) ? encUrlPreview : urlPreview;
|
||||
const [showDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||
const {
|
||||
hideActivity,
|
||||
messageLayout,
|
||||
messageSpacing,
|
||||
legacyUsernameColor,
|
||||
mediaAutoLoad,
|
||||
urlPreview,
|
||||
encUrlPreview,
|
||||
developerTools,
|
||||
hour24Clock,
|
||||
dateFormatString,
|
||||
} = useSettings();
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const direct = useIsDirectRoom();
|
||||
const showUrlPreview = encryptedRooms.has(roomId) ? encUrlPreview : urlPreview;
|
||||
|
||||
const ignoredUsersList = useIgnoredUsers();
|
||||
const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]);
|
||||
|
|
@ -321,7 +323,7 @@ export function TimelineView({ threadTimeline, threadInputRef }: TimelineViewPro
|
|||
onEditId={handleEdit}
|
||||
redacted={redacted.has(event.event_id)}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
showDeveloperTools={developerTools}
|
||||
memberPowerTag={getMemberPowerTag(event.sender)}
|
||||
accessibleTagColors={accessiblePowerTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
|
|
|
|||
|
|
@ -93,12 +93,82 @@ const useSettingsMenuItems = (): SettingsMenuItem[] =>
|
|||
[]
|
||||
);
|
||||
|
||||
type NavigationListProps = {
|
||||
activePage?: SettingsPages;
|
||||
setActivePage: (page: SettingsPages) => void;
|
||||
};
|
||||
function NavigationList({ activePage, setActivePage }: NavigationListProps) {
|
||||
const menuItems = useSettingsMenuItems();
|
||||
|
||||
return (
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
{menuItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
variant="Background"
|
||||
radii="400"
|
||||
aria-pressed={activePage === item.page}
|
||||
before={<Icon src={item.icon} size="100" filled={activePage === item.page} />}
|
||||
onClick={() => setActivePage(item.page)}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontWeight: activePage === item.page ? config.fontWeight.W600 : undefined,
|
||||
}}
|
||||
size="T300"
|
||||
truncate
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LogoutButton() {
|
||||
return (
|
||||
<UseStateProvider initial={false}>
|
||||
{(logout, setLogout) => (
|
||||
<>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
radii="Pill"
|
||||
before={<Icon src={Icons.Power} size="100" />}
|
||||
onClick={() => setLogout(true)}
|
||||
>
|
||||
<Text size="B400">Logout</Text>
|
||||
</Button>
|
||||
{logout && (
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
onDeactivate: () => setLogout(false),
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<LogoutDialog handleClose={() => setLogout(false)} />
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type SettingsProps = {
|
||||
initialPage?: SettingsPages;
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function Settings({ initialPage, requestClose }: SettingsProps) {
|
||||
const ct = useClient();
|
||||
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const { userId } = ct.session;
|
||||
const profile = useUserProfile(userId);
|
||||
|
|
@ -116,7 +186,6 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
|||
if (initialPage) return initialPage;
|
||||
return screenSize === ScreenSize.Mobile ? undefined : SettingsPages.GeneralPage;
|
||||
});
|
||||
const menuItems = useSettingsMenuItems();
|
||||
|
||||
const handlePageRequestClose = () => {
|
||||
if (screenSize === ScreenSize.Mobile) {
|
||||
|
|
@ -126,10 +195,12 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
|||
requestClose();
|
||||
};
|
||||
|
||||
const hideSidebar = screenSize === ScreenSize.Mobile && activePage !== undefined;
|
||||
|
||||
return (
|
||||
<PageRoot
|
||||
nav={
|
||||
screenSize === ScreenSize.Mobile && activePage !== undefined ? undefined : (
|
||||
hideSidebar ? undefined : (
|
||||
<PageNav size="300">
|
||||
<PageNavHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
|
|
@ -154,61 +225,10 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
|||
</PageNavHeader>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<PageNavContent>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
{menuItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
variant="Background"
|
||||
radii="400"
|
||||
aria-pressed={activePage === item.page}
|
||||
before={<Icon src={item.icon} size="100" filled={activePage === item.page} />}
|
||||
onClick={() => setActivePage(item.page)}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontWeight: activePage === item.page ? config.fontWeight.W600 : undefined,
|
||||
}}
|
||||
size="T300"
|
||||
truncate
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
<NavigationList activePage={activePage} setActivePage={setActivePage} />
|
||||
</PageNavContent>
|
||||
<Box style={{ padding: config.space.S200 }} shrink="No" direction="Column">
|
||||
<UseStateProvider initial={false}>
|
||||
{(logout, setLogout) => (
|
||||
<>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
radii="Pill"
|
||||
before={<Icon src={Icons.Power} size="100" />}
|
||||
onClick={() => setLogout(true)}
|
||||
>
|
||||
<Text size="B400">Logout</Text>
|
||||
</Button>
|
||||
{logout && (
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
onDeactivate: () => setLogout(false),
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<LogoutDialog handleClose={() => setLogout(false)} />
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
<Box style={{ padding: config.space.S200 }} shrink="No" direction="Column" gap="100">
|
||||
<LogoutButton />
|
||||
</Box>
|
||||
</Box>
|
||||
</PageNav>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { SequenceCard } from '../../../components/sequence-card';
|
|||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import {
|
||||
AccountDataEditor,
|
||||
AccountDataSubmitCallback,
|
||||
|
|
@ -22,7 +21,7 @@ type DeveloperToolsProps = {
|
|||
};
|
||||
export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
||||
const ct = useClient();
|
||||
const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools');
|
||||
const [developerTools, setDeveloperTools] = useSetting('developerTools');
|
||||
|
||||
const [expand, setExpend] = useState(false);
|
||||
const [editAccountData, setEditAccountData] = useState<AccountDataEvent | null>();
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import { SequenceCardStyle } from '../styles.css';
|
|||
import { LogoutDialog } from '../../../components/LogoutDialog';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useClient } from '../../../hooks/useClient';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
|
||||
|
|
@ -45,8 +44,8 @@ export function DeviceTilePlaceholder() {
|
|||
}
|
||||
|
||||
function DeviceActiveTime({ ts }: { ts: number }) {
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
const [dateFormatString] = useSetting('dateFormatString');
|
||||
|
||||
return (
|
||||
<Text className={BreakWord} size="T200">
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import FocusTrap from 'focus-trap-react';
|
|||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
||||
import { DateFormat, MessageLayout, MessageSpacing } from '../../../state/settings';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { KeySymbol } from '../../../utils/key-symbol';
|
||||
import { isMacOS } from '../../../utils/user-agent';
|
||||
|
|
@ -80,7 +80,7 @@ const ThemeSelector = as<'div', ThemeSelectorProps>(
|
|||
function SelectTheme({ disabled }: { disabled?: boolean }) {
|
||||
const themes = useThemes();
|
||||
const themeNames = useThemeNames();
|
||||
const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId');
|
||||
const [themeId, setThemeId] = useSetting('themeId');
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
||||
|
||||
|
|
@ -142,8 +142,8 @@ function SystemThemePreferences() {
|
|||
const themeKind = useSystemThemeKind();
|
||||
const themeNames = useThemeNames();
|
||||
const themes = useThemes();
|
||||
const [lightThemeId, setLightThemeId] = useSetting(settingsAtom, 'lightThemeId');
|
||||
const [darkThemeId, setDarkThemeId] = useSetting(settingsAtom, 'darkThemeId');
|
||||
const [lightThemeId, setLightThemeId] = useSetting('lightThemeId');
|
||||
const [darkThemeId, setDarkThemeId] = useSetting('darkThemeId');
|
||||
|
||||
const lightThemes = themes.filter((theme) => theme.kind === ThemeKind.Light);
|
||||
const darkThemes = themes.filter((theme) => theme.kind === ThemeKind.Dark);
|
||||
|
|
@ -260,7 +260,7 @@ function SystemThemePreferences() {
|
|||
}
|
||||
|
||||
function PageZoomInput() {
|
||||
const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom');
|
||||
const [pageZoom, setPageZoom] = useSetting('pageZoom');
|
||||
const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`);
|
||||
|
||||
const handleZoomChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||
|
|
@ -304,9 +304,9 @@ function PageZoomInput() {
|
|||
}
|
||||
|
||||
function Appearance() {
|
||||
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
||||
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
|
||||
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||
const [systemTheme, setSystemTheme] = useSetting('useSystemTheme');
|
||||
const [monochromeMode, setMonochromeMode] = useSetting('monochromeMode');
|
||||
const [twitterEmoji, setTwitterEmoji] = useSetting('twitterEmoji');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
|
|
@ -662,7 +662,7 @@ function PresetDateFormat({ value, onChange }: PresetDateFormatProps) {
|
|||
}
|
||||
|
||||
function SelectDateFormat() {
|
||||
const [dateFormatString, setDateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const [dateFormatString, setDateFormatString] = useSetting('dateFormatString');
|
||||
const [selectedDateFormat, setSelectedDateFormat] = useState(dateFormatString);
|
||||
const customDateFormat = selectedDateFormat === '';
|
||||
|
||||
|
|
@ -688,7 +688,7 @@ function SelectDateFormat() {
|
|||
}
|
||||
|
||||
function DateAndTime() {
|
||||
const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [hour24Clock, setHour24Clock] = useSetting('hour24Clock');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
|
|
@ -708,9 +708,9 @@ function DateAndTime() {
|
|||
}
|
||||
|
||||
function Editor() {
|
||||
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||
const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [enterForNewline, setEnterForNewline] = useSetting('enterForNewline');
|
||||
const [isMarkdown, setIsMarkdown] = useSetting('isMarkdown');
|
||||
const [hideActivity, setHideActivity] = useSetting('hideActivity');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
|
|
@ -743,7 +743,7 @@ function Editor() {
|
|||
|
||||
function SelectMessageLayout() {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout');
|
||||
const [messageLayout, setMessageLayout] = useSetting('messageLayout');
|
||||
const messageLayoutItems = useMessageLayoutItems();
|
||||
|
||||
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
|
|
@ -812,7 +812,7 @@ function SelectMessageLayout() {
|
|||
|
||||
function SelectMessageSpacing() {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const [messageSpacing, setMessageSpacing] = useSetting(settingsAtom, 'messageSpacing');
|
||||
const [messageSpacing, setMessageSpacing] = useSetting('messageSpacing');
|
||||
const messageSpacingItems = useMessageSpacingItems();
|
||||
|
||||
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
|
|
@ -880,22 +880,13 @@ function SelectMessageSpacing() {
|
|||
}
|
||||
|
||||
function Messages() {
|
||||
const [legacyUsernameColor, setLegacyUsernameColor] = useSetting(
|
||||
settingsAtom,
|
||||
'legacyUsernameColor'
|
||||
);
|
||||
const [hideMembershipEvents, setHideMembershipEvents] = useSetting(
|
||||
settingsAtom,
|
||||
'hideMembershipEvents'
|
||||
);
|
||||
const [hideNickAvatarEvents, setHideNickAvatarEvents] = useSetting(
|
||||
settingsAtom,
|
||||
'hideNickAvatarEvents'
|
||||
);
|
||||
const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
|
||||
const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
|
||||
const [legacyUsernameColor, setLegacyUsernameColor] = useSetting('legacyUsernameColor');
|
||||
const [hideMembershipEvents, setHideMembershipEvents] = useSetting('hideMembershipEvents');
|
||||
const [hideNickAvatarEvents, setHideNickAvatarEvents] = useSetting('hideNickAvatarEvents');
|
||||
const [mediaAutoLoad, setMediaAutoLoad] = useSetting('mediaAutoLoad');
|
||||
const [urlPreview, setUrlPreview] = useSetting('urlPreview');
|
||||
const [encUrlPreview, setEncUrlPreview] = useSetting('encUrlPreview');
|
||||
const [showHiddenEvents, setShowHiddenEvents] = useSetting('showHiddenEvents');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { SequenceCard } from '../../../components/sequence-card';
|
|||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { getNotificationState, usePermissionState } from '../../../hooks/usePermission';
|
||||
import { useEmailNotifications } from '../../../hooks/useEmailNotifications';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
|
|
@ -95,11 +94,8 @@ function EmailNotification() {
|
|||
|
||||
export function SystemNotification() {
|
||||
const notifPermission = usePermissionState('notifications', getNotificationState());
|
||||
const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications');
|
||||
const [isNotificationSounds, setIsNotificationSounds] = useSetting(
|
||||
settingsAtom,
|
||||
'isNotificationSounds'
|
||||
);
|
||||
const [showNotifications, setShowNotifications] = useSetting('showNotifications');
|
||||
const [isNotificationSounds, setIsNotificationSounds] = useSetting('isNotificationSounds');
|
||||
|
||||
const requestNotificationPermission = () => {
|
||||
window.Notification.requestPermission();
|
||||
|
|
|
|||
34
src/app/hooks/sw/useSessionRequest.ts
Normal file
34
src/app/hooks/sw/useSessionRequest.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Session } from '../../../client/sessions';
|
||||
|
||||
const getServiceWorker = () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
return navigator.serviceWorker;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const useSessionRequest = (session?: Session) => {
|
||||
useEffect(() => {
|
||||
const handleSWMessages = (ev: MessageEvent<any>) => {
|
||||
if (!session) return;
|
||||
|
||||
const { type, id } = ev.data ?? {};
|
||||
|
||||
if (type === 'session-request') {
|
||||
getServiceWorker()?.controller?.postMessage({
|
||||
type: 'session-request',
|
||||
id,
|
||||
baseUrl: session.baseUrl,
|
||||
accessToken: session.accessToken,
|
||||
});
|
||||
// sendSessionToSW();
|
||||
}
|
||||
};
|
||||
|
||||
getServiceWorker()?.addEventListener('message', handleSWMessages);
|
||||
return () => {
|
||||
getServiceWorker()?.removeEventListener('message', handleSWMessages);
|
||||
};
|
||||
}, [session]);
|
||||
};
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
/* eslint-disable no-continue */
|
||||
import { MatrixEvent, Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { MessageEvent, StateEvent } from '../../types/matrix/room';
|
||||
import { isMembershipChanged, reactionOrEditEvent } from '../utils/room';
|
||||
|
||||
export const useRoomLatestRenderedEvent = (room: Room) => {
|
||||
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
|
||||
const [hideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents');
|
||||
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
|
||||
const [latestEvent, setLatestEvent] = useState<MatrixEvent>();
|
||||
|
||||
useEffect(() => {
|
||||
const getLatestEvent = (): MatrixEvent | undefined => {
|
||||
const liveEvents = room.getLiveTimeline().getEvents();
|
||||
for (let i = liveEvents.length - 1; i >= 0; i -= 1) {
|
||||
const evt = liveEvents[i];
|
||||
|
||||
if (!evt) continue;
|
||||
if (reactionOrEditEvent(evt)) continue;
|
||||
if (evt.getType() === StateEvent.RoomMember) {
|
||||
const membershipChanged = isMembershipChanged(evt);
|
||||
if (membershipChanged && hideMembershipEvents) continue;
|
||||
if (!membershipChanged && hideNickAvatarEvents) continue;
|
||||
return evt;
|
||||
}
|
||||
|
||||
if (
|
||||
evt.getType() === MessageEvent.RoomMessage ||
|
||||
evt.getType() === MessageEvent.RoomMessageEncrypted ||
|
||||
evt.getType() === MessageEvent.Sticker ||
|
||||
evt.getType() === StateEvent.RoomName ||
|
||||
evt.getType() === StateEvent.RoomTopic ||
|
||||
evt.getType() === StateEvent.RoomAvatar
|
||||
) {
|
||||
return evt;
|
||||
}
|
||||
|
||||
if (showHiddenEvents) return evt;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const handleTimelineEvent: RoomEventHandlerMap[RoomEvent.Timeline] = () => {
|
||||
setLatestEvent(getLatestEvent());
|
||||
};
|
||||
setLatestEvent(getLatestEvent());
|
||||
|
||||
room.on(RoomEvent.Timeline, handleTimelineEvent);
|
||||
return () => {
|
||||
room.removeListener(RoomEvent.Timeline, handleTimelineEvent);
|
||||
};
|
||||
}, [room, hideMembershipEvents, hideNickAvatarEvents, showHiddenEvents]);
|
||||
|
||||
return latestEvent;
|
||||
};
|
||||
|
|
@ -11,7 +11,6 @@ import { getOrphanParents, guessPerfectParent } from '../utils/room';
|
|||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||
import { mDirectAtom } from '../state/mDirectList';
|
||||
import { useSelectedSpace } from './router/useSelectedSpace';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { useClient } from './useClient';
|
||||
import { getJoinRoomStateId } from '../../client/storage/collections';
|
||||
|
|
@ -23,7 +22,7 @@ export const useRoomNavigate = () => {
|
|||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const spaceSelectedId = useSelectedSpace();
|
||||
const [developerTools] = useSetting(settingsAtom, 'developerTools');
|
||||
const [developerTools] = useSetting('developerTools');
|
||||
|
||||
const navigateSpace = useCallback(
|
||||
(roomId: string) => {
|
||||
|
|
|
|||
23
src/app/hooks/useSessions.ts
Normal file
23
src/app/hooks/useSessions.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { useAtomValue } from 'jotai';
|
||||
import { createContext, useContext } from 'react';
|
||||
import { sessionsAtom } from '../state/sessions';
|
||||
import { Session } from '../../client/sessions';
|
||||
|
||||
export const useSessions = () => {
|
||||
const sessions = useAtomValue(sessionsAtom);
|
||||
|
||||
return sessions;
|
||||
};
|
||||
|
||||
const SessionContext = createContext<Session | null>(null);
|
||||
export const SessionContextProvider = SessionContext.Provider;
|
||||
|
||||
export const useSession = () => {
|
||||
const session = useContext(SessionContext);
|
||||
|
||||
if (!session) {
|
||||
throw new Error('Session is not provided!');
|
||||
}
|
||||
|
||||
return session;
|
||||
};
|
||||
|
|
@ -2,7 +2,6 @@ import { lightTheme } from 'folds';
|
|||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
||||
import { butterTheme, darkTheme, silverTheme } from '../../colors.css';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
|
||||
export enum ThemeKind {
|
||||
|
|
@ -78,10 +77,10 @@ export const useSystemThemeKind = (): ThemeKind => {
|
|||
export const useActiveTheme = (): Theme => {
|
||||
const systemThemeKind = useSystemThemeKind();
|
||||
const themes = useThemes();
|
||||
const [systemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
||||
const [themeId] = useSetting(settingsAtom, 'themeId');
|
||||
const [lightThemeId] = useSetting(settingsAtom, 'lightThemeId');
|
||||
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
|
||||
const [systemTheme] = useSetting('useSystemTheme');
|
||||
const [themeId] = useSetting('themeId');
|
||||
const [lightThemeId] = useSetting('lightThemeId');
|
||||
const [darkThemeId] = useSetting('darkThemeId');
|
||||
|
||||
if (!systemTheme) {
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getProfile, GetProfileOK } from 'neurone/lib/api/matrix/client/v3/profile';
|
||||
import { parseMatrixResp } from 'neurone/lib/matrix-request';
|
||||
import { MatrixRequest, parseMatrixResp } from 'neurone/lib/matrix-request';
|
||||
import { ObjectReader } from 'neurone/lib/utils';
|
||||
import { useClient } from './useClient';
|
||||
|
||||
|
|
@ -24,11 +24,13 @@ export type UserProfile = {
|
|||
pronouns?: UserPronoun[];
|
||||
biography?: UserBiography;
|
||||
};
|
||||
export const useUserProfile = (userId: string): UserProfile | undefined => {
|
||||
const ct = useClient();
|
||||
|
||||
export const useUserProfileRequest = (
|
||||
userId: string,
|
||||
request: MatrixRequest
|
||||
): UserProfile | undefined => {
|
||||
const fetchProfile = useCallback(async (): Promise<UserProfile> => {
|
||||
const response = await ct.requestAuthJSON(
|
||||
const response = await request(
|
||||
getProfile({
|
||||
pathParams: { userId },
|
||||
})
|
||||
|
|
@ -75,7 +77,7 @@ export const useUserProfile = (userId: string): UserProfile | undefined => {
|
|||
}
|
||||
: undefined,
|
||||
};
|
||||
}, [ct, userId]);
|
||||
}, [request, userId]);
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['profile', userId],
|
||||
|
|
@ -87,3 +89,9 @@ export const useUserProfile = (userId: string): UserProfile | undefined => {
|
|||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useUserProfile = (userId: string): UserProfile | undefined => {
|
||||
const ct = useClient();
|
||||
|
||||
return useUserProfileRequest(userId, ct.requestAuthJSON);
|
||||
};
|
||||
|
|
|
|||
200
src/app/pages/AccountSelector.tsx
Normal file
200
src/app/pages/AccountSelector.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import React, { useMemo, useRef } from 'react';
|
||||
import { Avatar, Box, Button, config, Icon, Icons, Text } from 'folds';
|
||||
import { Link, Navigate, Outlet, useNavigate } from 'react-router-dom';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { AuthLayout, AuthLogo } from '../components/auth-layout';
|
||||
import { SequenceCard } from '../components/sequence-card';
|
||||
import { SettingTile } from '../components/setting-tile';
|
||||
import {
|
||||
getAppPathFromHref,
|
||||
getHomePath,
|
||||
getLoginPath,
|
||||
getOriginBaseUrl,
|
||||
getRegisterPath,
|
||||
} from './pathUtils';
|
||||
import { getMxIdLocalPart, getMxIdServer } from '../utils/matrix';
|
||||
import { UserAvatar } from '../components/user-avatar';
|
||||
import { SessionContextProvider, useSessions } from '../hooks/useSessions';
|
||||
import { useUnauthThemeManager } from './ThemeManager';
|
||||
import { sessionUserIdAtom } from '../state/sessions';
|
||||
import { useClientConfig } from '../hooks/useClientConfig';
|
||||
import {
|
||||
deleteAfterLoginRedirectPath,
|
||||
getAfterLoginRedirectPath,
|
||||
setAfterLoginRedirectPath,
|
||||
} from './afterLoginRedirectPath';
|
||||
import { Session } from '../../client/sessions';
|
||||
import { OrDivider } from '../components/OrDivider';
|
||||
import { getAuthJSONRequest } from '../../client/request';
|
||||
import { useUserProfileRequest } from '../hooks/useUserProfile';
|
||||
import { mxcToHttp } from '../../client/mxcToHttp';
|
||||
import { useSessionRequest } from '../hooks/sw/useSessionRequest';
|
||||
|
||||
function AccountCard({ session }: { session: Session }) {
|
||||
const { userId } = session;
|
||||
const navigate = useNavigate();
|
||||
const [sessionUserId, setSessionUserId] = useAtom(sessionUserIdAtom);
|
||||
|
||||
const request = useMemo(
|
||||
() => getAuthJSONRequest(session.baseUrl, session.accessToken),
|
||||
[session]
|
||||
);
|
||||
|
||||
const profile = useUserProfileRequest(userId, request);
|
||||
|
||||
const username = getMxIdLocalPart(userId) ?? userId;
|
||||
const server = getMxIdServer(userId) ?? userId;
|
||||
|
||||
const displayName = profile?.displayName ?? username;
|
||||
const avatarUrl = profile?.avatarUrl
|
||||
? mxcToHttp(session.baseUrl, profile.avatarUrl, true).thumbnail({
|
||||
width: 96,
|
||||
height: 96,
|
||||
method: 'crop',
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const handleClick = () => {
|
||||
if (sessionUserId !== userId) {
|
||||
setSessionUserId(userId);
|
||||
}
|
||||
const path = getAfterLoginRedirectPath();
|
||||
if (path) deleteAfterLoginRedirectPath();
|
||||
navigate(path ?? getHomePath());
|
||||
};
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
direction="Column"
|
||||
style={{ padding: `${config.space.S300} ${config.space.S400}` }}
|
||||
variant="SurfaceVariant"
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<SettingTile
|
||||
before={
|
||||
<Avatar size="300" radii="400">
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
alt={userId}
|
||||
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Box gap="200" justifyContent="SpaceBetween" alignItems="Center">
|
||||
<Text truncate>
|
||||
<b>{displayName}</b>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="T200" truncate>
|
||||
{username}
|
||||
</Text>
|
||||
<Text size="L400" truncate>
|
||||
{server}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</SettingTile>
|
||||
</SequenceCard>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountSelector() {
|
||||
useUnauthThemeManager();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const sessions = useSessions();
|
||||
useSessionRequest(sessions[0]);
|
||||
|
||||
if (sessions.length === 0) {
|
||||
return <Navigate to={getLoginPath()} replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout hideHeader>
|
||||
<Box style={{ position: 'relative' }} direction="Column" gap="500">
|
||||
<span data-spacing-node />
|
||||
<Box direction="Column" gap="400" alignItems="Center">
|
||||
<AuthLogo large />
|
||||
<Box direction="Column" gap="100" alignItems="Center">
|
||||
<Text size="H3" priority="400" align="Center">
|
||||
Welcome to Cinny
|
||||
</Text>
|
||||
<Text size="T300" priority="400" align="Center">
|
||||
Choose an account to continue.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<span data-spacing-node />
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400" priority="400">
|
||||
Accounts
|
||||
</Text>
|
||||
{sessions.map((session) => (
|
||||
<AccountCard key={session.userId} session={session} />
|
||||
))}
|
||||
</Box>
|
||||
<OrDivider />
|
||||
<Box direction="Column" gap="300">
|
||||
<Button
|
||||
type="button"
|
||||
variant="Primary"
|
||||
size="500"
|
||||
before={<Icon size="400" src={Icons.Plus} />}
|
||||
onClick={() => navigate(getLoginPath())}
|
||||
>
|
||||
<Text as="span" size="B500">
|
||||
Add Account
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
<span data-spacing-node />
|
||||
<Text size="T300" align="Center">
|
||||
Do not have an account? <Link to={getRegisterPath()}>Register</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClientAccountSelector() {
|
||||
const clientConfig = useClientConfig();
|
||||
|
||||
const prevUserIdRef = useRef<string>();
|
||||
|
||||
const userId = useAtomValue(sessionUserIdAtom);
|
||||
const prevUserId = prevUserIdRef.current;
|
||||
prevUserIdRef.current = userId;
|
||||
|
||||
const sessions = useSessions();
|
||||
const session = useMemo(() => sessions.find((s) => userId === s.userId), [userId, sessions]);
|
||||
|
||||
useSessionRequest(session ?? sessions[0]);
|
||||
|
||||
if (!session) {
|
||||
return <AccountSelector />;
|
||||
}
|
||||
|
||||
if (sessions.length === 0) {
|
||||
const afterLoginPath = getAppPathFromHref(
|
||||
getOriginBaseUrl(clientConfig.hashRouter),
|
||||
window.location.href
|
||||
);
|
||||
if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath);
|
||||
|
||||
return <Navigate to={getLoginPath()} replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SessionContextProvider key={session.userId} value={session}>
|
||||
<Outlet />
|
||||
{/* reset url from previous account */}
|
||||
{prevUserId && prevUserId !== userId && <Navigate to={getHomePath()} />}
|
||||
</SessionContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from 'react-router-dom';
|
||||
|
||||
import { ClientConfig } from '../hooks/useClientConfig';
|
||||
import { AuthLayout, Login, Register, ResetPassword } from './auth';
|
||||
import { AuthRoot, Login, Register, ResetPassword } from './auth';
|
||||
import {
|
||||
DIRECT_PATH,
|
||||
EXPLORE_PATH,
|
||||
|
|
@ -30,22 +30,13 @@ import {
|
|||
_SERVER_PATH,
|
||||
CREATE_PATH,
|
||||
} from './paths';
|
||||
import {
|
||||
getAppPathFromHref,
|
||||
getExploreFeaturedPath,
|
||||
getHomePath,
|
||||
getInboxNotificationsPath,
|
||||
getLoginPath,
|
||||
getOriginBaseUrl,
|
||||
getSpaceLobbyPath,
|
||||
} from './pathUtils';
|
||||
import { getExploreFeaturedPath, getInboxNotificationsPath, getSpaceLobbyPath } from './pathUtils';
|
||||
import { ClientBindAtoms, ClientLayout, ClientRoot } from './client';
|
||||
import { Home, HomeRouteRoomProvider, HomeSearch } from './client/home';
|
||||
import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
|
||||
import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
|
||||
import { Explore, FeaturedRooms, PublicRooms } from './client/explore';
|
||||
import { Notifications, Inbox, Invites } from './client/inbox';
|
||||
import { setAfterLoginRedirectPath } from './afterLoginRedirectPath';
|
||||
import { Room } from '../features/room';
|
||||
import { Lobby } from '../features/lobby';
|
||||
import { WelcomePage } from './client/WelcomePage';
|
||||
|
|
@ -55,7 +46,7 @@ import { ScreenSize } from '../hooks/useScreenSize';
|
|||
import { MobileFriendlyPageNav, MobileFriendlyClientNav } from './MobileFriendly';
|
||||
import { ClientInitStorageAtom } from './client/ClientInitStorageAtom';
|
||||
import { ClientNonUIFeatures } from './client/ClientNonUIFeatures';
|
||||
import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager';
|
||||
import { AuthRouteThemeManager } from './ThemeManager';
|
||||
import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification';
|
||||
import { RoomSettingsRenderer } from '../features/room-settings';
|
||||
import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
|
||||
|
|
@ -66,9 +57,9 @@ import { HomeCreateRoom } from './client/home/CreateRoom';
|
|||
import { Create } from './client/create';
|
||||
import { CreateSpaceModalRenderer } from '../features/create-space';
|
||||
import { SearchModalRenderer } from '../features/search';
|
||||
import { getFallbackSession } from '../state/sessions';
|
||||
import { CallStatusRenderer } from '../features/call-status';
|
||||
import { CallEmbedProvider } from '../components/CallEmbedProvider';
|
||||
import { AccountSelector, ClientAccountSelector } from './AccountSelector';
|
||||
|
||||
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
|
||||
const { hashRouter } = clientConfig;
|
||||
|
|
@ -76,217 +67,185 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
|
||||
const routes = createRoutesFromElements(
|
||||
<Route>
|
||||
<Route
|
||||
index
|
||||
loader={() => {
|
||||
if (getFallbackSession()) return redirect(getHomePath());
|
||||
const afterLoginPath = getAppPathFromHref(getOriginBaseUrl(), window.location.href);
|
||||
if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath);
|
||||
return redirect(getLoginPath());
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
loader={() => {
|
||||
if (getFallbackSession()) {
|
||||
return redirect(getHomePath());
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
element={
|
||||
<>
|
||||
<AuthLayout />
|
||||
<UnAuthRouteThemeManager />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Route index element={<AccountSelector />} />
|
||||
<Route element={<AuthRoot />}>
|
||||
<Route path={LOGIN_PATH} element={<Login />} />
|
||||
<Route path={REGISTER_PATH} element={<Register />} />
|
||||
<Route path={RESET_PASSWORD_PATH} element={<ResetPassword />} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
loader={() => {
|
||||
const session = getFallbackSession();
|
||||
if (!session) {
|
||||
const afterLoginPath = getAppPathFromHref(
|
||||
getOriginBaseUrl(hashRouter),
|
||||
window.location.href
|
||||
);
|
||||
if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath);
|
||||
return redirect(getLoginPath());
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
element={
|
||||
<AuthRouteThemeManager>
|
||||
<ClientRoot>
|
||||
<ClientInitStorageAtom>
|
||||
<ClientRoomsNotificationPreferences>
|
||||
<ClientBindAtoms />
|
||||
<ClientNonUIFeatures />
|
||||
<ClientLayout
|
||||
nav={
|
||||
<MobileFriendlyClientNav>
|
||||
<SidebarNav />
|
||||
</MobileFriendlyClientNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</ClientLayout>
|
||||
<SearchModalRenderer />
|
||||
<UserRoomProfileRenderer />
|
||||
<CreateRoomModalRenderer />
|
||||
<CreateSpaceModalRenderer />
|
||||
<RoomSettingsRenderer />
|
||||
<SpaceSettingsRenderer />
|
||||
<ReceiveSelfDeviceVerification />
|
||||
</ClientRoomsNotificationPreferences>
|
||||
</ClientInitStorageAtom>
|
||||
</ClientRoot>
|
||||
</AuthRouteThemeManager>
|
||||
}
|
||||
>
|
||||
<Route element={<ClientAccountSelector />}>
|
||||
<Route
|
||||
path={HOME_PATH}
|
||||
element={
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={HOME_PATH}>
|
||||
<Home />
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</PageRoot>
|
||||
<ClientInitStorageAtom>
|
||||
<AuthRouteThemeManager>
|
||||
<ClientRoot>
|
||||
<ClientRoomsNotificationPreferences>
|
||||
<ClientBindAtoms />
|
||||
<ClientNonUIFeatures />
|
||||
<ClientLayout
|
||||
nav={
|
||||
<MobileFriendlyClientNav>
|
||||
<SidebarNav />
|
||||
</MobileFriendlyClientNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</ClientLayout>
|
||||
<SearchModalRenderer />
|
||||
<UserRoomProfileRenderer />
|
||||
<CreateRoomModalRenderer />
|
||||
<CreateSpaceModalRenderer />
|
||||
<RoomSettingsRenderer />
|
||||
<SpaceSettingsRenderer />
|
||||
<ReceiveSelfDeviceVerification />
|
||||
</ClientRoomsNotificationPreferences>
|
||||
</ClientRoot>
|
||||
</AuthRouteThemeManager>
|
||||
</ClientInitStorageAtom>
|
||||
}
|
||||
>
|
||||
{mobile ? null : <Route index element={<WelcomePage />} />}
|
||||
<Route path={_CREATE_PATH} element={<HomeCreateRoom />} />
|
||||
<Route path={_JOIN_PATH} element={<p>join</p>} />
|
||||
{/* <Route path={_SEARCH_PATH} element={<HomeSearch />} /> */}
|
||||
<Route
|
||||
path={_ROOM_PATH}
|
||||
path={HOME_PATH}
|
||||
element={
|
||||
<HomeRouteRoomProvider>
|
||||
<Room />
|
||||
</HomeRouteRoomProvider>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={DIRECT_PATH}
|
||||
element={
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={DIRECT_PATH}>
|
||||
<Direct />
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</PageRoot>
|
||||
}
|
||||
>
|
||||
{mobile ? null : <Route index element={<WelcomePage />} />}
|
||||
<Route path={_CREATE_PATH} element={<DirectCreate />} />
|
||||
<Route
|
||||
path={_ROOM_PATH}
|
||||
element={
|
||||
<DirectRouteRoomProvider>
|
||||
<Room />
|
||||
</DirectRouteRoomProvider>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={SPACE_PATH}
|
||||
element={
|
||||
<RouteSpaceProvider>
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={SPACE_PATH}>
|
||||
<Space />
|
||||
<MobileFriendlyPageNav path={HOME_PATH}>
|
||||
<Home />
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</PageRoot>
|
||||
</RouteSpaceProvider>
|
||||
}
|
||||
>
|
||||
{mobile ? null : (
|
||||
<Route
|
||||
index
|
||||
loader={({ params }) => {
|
||||
const { spaceIdOrAlias } = params;
|
||||
if (spaceIdOrAlias) {
|
||||
return redirect(getSpaceLobbyPath(spaceIdOrAlias));
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_LOBBY_PATH} element={<Lobby />} />
|
||||
{/*<Route path={_SEARCH_PATH} element={<SpaceSearch />} /> */}
|
||||
<Route
|
||||
path={_ROOM_PATH}
|
||||
element={
|
||||
<SpaceRouteRoomProvider>
|
||||
<Room />
|
||||
</SpaceRouteRoomProvider>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={EXPLORE_PATH}
|
||||
element={
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={EXPLORE_PATH}>
|
||||
<Explore />
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</PageRoot>
|
||||
}
|
||||
>
|
||||
{mobile ? null : (
|
||||
>
|
||||
{mobile ? null : <Route index element={<WelcomePage />} />}
|
||||
<Route path={_CREATE_PATH} element={<HomeCreateRoom />} />
|
||||
<Route path={_JOIN_PATH} element={<p>join</p>} />
|
||||
{/* <Route path={_SEARCH_PATH} element={<HomeSearch />} /> */}
|
||||
<Route
|
||||
index
|
||||
loader={() => redirect(getExploreFeaturedPath())}
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_FEATURED_PATH} element={<FeaturedRooms />} />
|
||||
<Route path={_SERVER_PATH} element={<PublicRooms />} />
|
||||
</Route>
|
||||
<Route path={CREATE_PATH} element={<Create />} />
|
||||
<Route
|
||||
path={INBOX_PATH}
|
||||
element={
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={INBOX_PATH}>
|
||||
<Inbox />
|
||||
</MobileFriendlyPageNav>
|
||||
path={_ROOM_PATH}
|
||||
element={
|
||||
<HomeRouteRoomProvider>
|
||||
<Room />
|
||||
</HomeRouteRoomProvider>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</PageRoot>
|
||||
}
|
||||
>
|
||||
{mobile ? null : (
|
||||
<Route
|
||||
index
|
||||
loader={() => redirect(getInboxNotificationsPath())}
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_NOTIFICATIONS_PATH} element={<Notifications />} />
|
||||
<Route path={_INVITES_PATH} element={<Invites />} />
|
||||
</Route>
|
||||
<Route
|
||||
path={DIRECT_PATH}
|
||||
element={
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={DIRECT_PATH}>
|
||||
<Direct />
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</PageRoot>
|
||||
}
|
||||
>
|
||||
{mobile ? null : <Route index element={<WelcomePage />} />}
|
||||
<Route path={_CREATE_PATH} element={<DirectCreate />} />
|
||||
<Route
|
||||
path={_ROOM_PATH}
|
||||
element={
|
||||
<DirectRouteRoomProvider>
|
||||
<Room />
|
||||
</DirectRouteRoomProvider>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={SPACE_PATH}
|
||||
element={
|
||||
<RouteSpaceProvider>
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={SPACE_PATH}>
|
||||
<Space />
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</PageRoot>
|
||||
</RouteSpaceProvider>
|
||||
}
|
||||
>
|
||||
{mobile ? null : (
|
||||
<Route
|
||||
index
|
||||
loader={({ params }) => {
|
||||
const { spaceIdOrAlias } = params;
|
||||
if (spaceIdOrAlias) {
|
||||
return redirect(getSpaceLobbyPath(spaceIdOrAlias));
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_LOBBY_PATH} element={<Lobby />} />
|
||||
{/*<Route path={_SEARCH_PATH} element={<SpaceSearch />} /> */}
|
||||
<Route
|
||||
path={_ROOM_PATH}
|
||||
element={
|
||||
<SpaceRouteRoomProvider>
|
||||
<Room />
|
||||
</SpaceRouteRoomProvider>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={EXPLORE_PATH}
|
||||
element={
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={EXPLORE_PATH}>
|
||||
<Explore />
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</PageRoot>
|
||||
}
|
||||
>
|
||||
{mobile ? null : (
|
||||
<Route
|
||||
index
|
||||
loader={() => redirect(getExploreFeaturedPath())}
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_FEATURED_PATH} element={<FeaturedRooms />} />
|
||||
<Route path={_SERVER_PATH} element={<PublicRooms />} />
|
||||
</Route>
|
||||
<Route path={CREATE_PATH} element={<Create />} />
|
||||
<Route
|
||||
path={INBOX_PATH}
|
||||
element={
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={INBOX_PATH}>
|
||||
<Inbox />
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</PageRoot>
|
||||
}
|
||||
>
|
||||
{mobile ? null : (
|
||||
<Route
|
||||
index
|
||||
loader={() => redirect(getInboxNotificationsPath())}
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_NOTIFICATIONS_PATH} element={<Notifications />} />
|
||||
<Route path={_INVITES_PATH} element={<Invites />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="/*" element={<p>Page not found</p>} />
|
||||
|
|
|
|||
|
|
@ -9,39 +9,45 @@ import {
|
|||
useSystemThemeKind,
|
||||
} from '../hooks/useTheme';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { ContainerColor } from '../styles/ContainerColor.css';
|
||||
|
||||
export function UnAuthRouteThemeManager() {
|
||||
export const useUnauthThemeManager = () => {
|
||||
const systemThemeKind = useSystemThemeKind();
|
||||
|
||||
useEffect(() => {
|
||||
document.body.className = '';
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
document.documentElement.className = '';
|
||||
document.documentElement.classList.add(
|
||||
configClass,
|
||||
varsClass,
|
||||
...ContainerColor({ variant: 'Background' }).split(' ')
|
||||
);
|
||||
if (systemThemeKind === ThemeKind.Dark) {
|
||||
document.body.classList.add(...DarkTheme.classNames);
|
||||
document.documentElement.classList.add(...DarkTheme.classNames);
|
||||
}
|
||||
if (systemThemeKind === ThemeKind.Light) {
|
||||
document.body.classList.add(...LightTheme.classNames);
|
||||
document.documentElement.classList.add(...LightTheme.classNames);
|
||||
}
|
||||
}, [systemThemeKind]);
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
||||
const activeTheme = useActiveTheme();
|
||||
const [monochromeMode] = useSetting(settingsAtom, 'monochromeMode');
|
||||
const [monochromeMode] = useSetting('monochromeMode');
|
||||
|
||||
useEffect(() => {
|
||||
document.body.className = '';
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
document.documentElement.className = '';
|
||||
document.documentElement.classList.add(
|
||||
configClass,
|
||||
varsClass,
|
||||
...ContainerColor({ variant: 'Background' }).split(' ')
|
||||
);
|
||||
|
||||
document.body.classList.add(...activeTheme.classNames);
|
||||
document.documentElement.classList.add(...activeTheme.classNames);
|
||||
|
||||
if (monochromeMode) {
|
||||
document.body.style.filter = 'grayscale(1)';
|
||||
document.documentElement.style.filter = 'grayscale(1)';
|
||||
} else {
|
||||
document.body.style.filter = '';
|
||||
document.documentElement.style.filter = '';
|
||||
}
|
||||
}, [activeTheme, monochromeMode]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,209 +0,0 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Box, Header, Scroll, Spinner, Text, color } from 'folds';
|
||||
import {
|
||||
Outlet,
|
||||
generatePath,
|
||||
matchPath,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { AuthFooter } from './AuthFooter';
|
||||
import * as css from './styles.css';
|
||||
import * as PatternsCss from '../../styles/Patterns.css';
|
||||
import {
|
||||
clientAllowedServer,
|
||||
clientDefaultServer,
|
||||
useClientConfig,
|
||||
} from '../../hooks/useClientConfig';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH } from '../paths';
|
||||
import CinnySVG from '../../../../public/res/svg/cinny.svg';
|
||||
import { ServerPicker } from './ServerPicker';
|
||||
import { AutoDiscoveryAction, autoDiscovery } from '../../cs-api';
|
||||
import { SpecVersionsLoader } from '../../components/SpecVersionsLoader';
|
||||
import { SpecVersionsProvider } from '../../hooks/useSpecVersions';
|
||||
import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo';
|
||||
import { AuthFlowsLoader } from '../../components/AuthFlowsLoader';
|
||||
import { AuthFlowsProvider } from '../../hooks/useAuthFlows';
|
||||
import { AuthServerProvider } from '../../hooks/useAuthServer';
|
||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||
|
||||
const currentAuthPath = (pathname: string): string => {
|
||||
if (matchPath(LOGIN_PATH, pathname)) {
|
||||
return LOGIN_PATH;
|
||||
}
|
||||
if (matchPath(RESET_PASSWORD_PATH, pathname)) {
|
||||
return RESET_PASSWORD_PATH;
|
||||
}
|
||||
if (matchPath(REGISTER_PATH, pathname)) {
|
||||
return REGISTER_PATH;
|
||||
}
|
||||
return LOGIN_PATH;
|
||||
};
|
||||
|
||||
function AuthLayoutLoading({ message }: { message: string }) {
|
||||
return (
|
||||
<Box justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Spinner size="100" variant="Secondary" />
|
||||
<Text align="Center" size="T300">
|
||||
{message}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthLayoutError({ message }: { message: string }) {
|
||||
return (
|
||||
<Box justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Text align="Center" style={{ color: color.Critical.Main }} size="T300">
|
||||
{message}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthLayout() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { server: urlEncodedServer } = useParams();
|
||||
|
||||
const clientConfig = useClientConfig();
|
||||
|
||||
const defaultServer = clientDefaultServer(clientConfig);
|
||||
let server: string = urlEncodedServer ? tryDecodeURIComponent(urlEncodedServer) : defaultServer;
|
||||
|
||||
if (!clientAllowedServer(clientConfig, server)) {
|
||||
server = defaultServer;
|
||||
}
|
||||
|
||||
const [discoveryState, discoverServer] = useAsyncCallback(
|
||||
useCallback(async (serverName: string) => {
|
||||
const response = await autoDiscovery(fetch, serverName);
|
||||
return {
|
||||
serverName,
|
||||
response,
|
||||
};
|
||||
}, [])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (server) discoverServer(server);
|
||||
}, [discoverServer, server]);
|
||||
|
||||
// if server is mismatches with path server, update path
|
||||
useEffect(() => {
|
||||
if (!urlEncodedServer || tryDecodeURIComponent(urlEncodedServer) !== server) {
|
||||
navigate(
|
||||
generatePath(currentAuthPath(location.pathname), {
|
||||
server: encodeURIComponent(server),
|
||||
}),
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
}, [urlEncodedServer, navigate, location, server]);
|
||||
|
||||
const selectServer = useCallback(
|
||||
(newServer: string) => {
|
||||
if (newServer === server) {
|
||||
if (discoveryState.status === AsyncStatus.Loading) return;
|
||||
discoverServer(server);
|
||||
return;
|
||||
}
|
||||
navigate(
|
||||
generatePath(currentAuthPath(location.pathname), { server: encodeURIComponent(newServer) })
|
||||
);
|
||||
},
|
||||
[navigate, location, discoveryState, server, discoverServer]
|
||||
);
|
||||
|
||||
const [autoDiscoveryError, autoDiscoveryInfo] =
|
||||
discoveryState.status === AsyncStatus.Success ? discoveryState.data.response : [];
|
||||
|
||||
return (
|
||||
<Scroll variant="Background" visibility="Hover" size="0" hideTrack>
|
||||
<Box
|
||||
className={classNames(css.AuthLayout, PatternsCss.BackgroundDotPattern)}
|
||||
direction="Column"
|
||||
alignItems="Center"
|
||||
justifyContent="SpaceBetween"
|
||||
gap="400"
|
||||
>
|
||||
<Box direction="Column" className={css.AuthCard}>
|
||||
<Header className={css.AuthHeader} size="600" variant="Surface">
|
||||
<Box grow="Yes" direction="Row" gap="300" alignItems="Center">
|
||||
<img className={css.AuthLogo} src={CinnySVG} alt="Cinny Logo" />
|
||||
<Text size="H3">Cinny</Text>
|
||||
</Box>
|
||||
</Header>
|
||||
<Box className={css.AuthCardContent} direction="Column">
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="label" size="L400" priority="300">
|
||||
Homeserver
|
||||
</Text>
|
||||
<ServerPicker
|
||||
server={server}
|
||||
serverList={clientConfig.homeserverList ?? []}
|
||||
allowCustomServer={clientConfig.allowCustomHomeservers}
|
||||
onServerChange={selectServer}
|
||||
/>
|
||||
</Box>
|
||||
{discoveryState.status === AsyncStatus.Loading && (
|
||||
<AuthLayoutLoading message="Looking for homeserver..." />
|
||||
)}
|
||||
{discoveryState.status === AsyncStatus.Error && (
|
||||
<AuthLayoutError message="Failed to find homeserver." />
|
||||
)}
|
||||
{autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && (
|
||||
<AuthLayoutError
|
||||
message={`Failed to connect. Homeserver configuration found with ${autoDiscoveryError.host} appears unusable.`}
|
||||
/>
|
||||
)}
|
||||
{autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && (
|
||||
<AuthLayoutError message="Failed to connect. Homeserver configuration base_url appears invalid." />
|
||||
)}
|
||||
{discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && (
|
||||
<AuthServerProvider value={discoveryState.data.serverName}>
|
||||
<AutoDiscoveryInfoProvider value={autoDiscoveryInfo}>
|
||||
<SpecVersionsLoader
|
||||
baseUrl={autoDiscoveryInfo['m.homeserver'].base_url}
|
||||
fallback={() => (
|
||||
<AuthLayoutLoading
|
||||
message={`Connecting to ${autoDiscoveryInfo['m.homeserver'].base_url}`}
|
||||
/>
|
||||
)}
|
||||
error={() => (
|
||||
<AuthLayoutError message="Failed to connect. Either homeserver is unavailable at this moment or does not exist." />
|
||||
)}
|
||||
>
|
||||
{(specVersions) => (
|
||||
<SpecVersionsProvider value={specVersions}>
|
||||
<AuthFlowsLoader
|
||||
fallback={() => (
|
||||
<AuthLayoutLoading message="Loading authentication flow..." />
|
||||
)}
|
||||
error={() => (
|
||||
<AuthLayoutError message="Failed to get authentication flow information." />
|
||||
)}
|
||||
>
|
||||
{(authFlows) => (
|
||||
<AuthFlowsProvider value={authFlows}>
|
||||
<Outlet />
|
||||
</AuthFlowsProvider>
|
||||
)}
|
||||
</AuthFlowsLoader>
|
||||
</SpecVersionsProvider>
|
||||
)}
|
||||
</SpecVersionsLoader>
|
||||
</AutoDiscoveryInfoProvider>
|
||||
</AuthServerProvider>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<AuthFooter />
|
||||
</Box>
|
||||
</Scroll>
|
||||
);
|
||||
}
|
||||
186
src/app/pages/auth/AuthRoot.tsx
Normal file
186
src/app/pages/auth/AuthRoot.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Box, Spinner, Text, color } from 'folds';
|
||||
import {
|
||||
Outlet,
|
||||
generatePath,
|
||||
matchPath,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import {
|
||||
clientAllowedServer,
|
||||
clientDefaultServer,
|
||||
useClientConfig,
|
||||
} from '../../hooks/useClientConfig';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH } from '../paths';
|
||||
import { ServerPicker } from './ServerPicker';
|
||||
import { AutoDiscoveryAction, autoDiscovery } from '../../cs-api';
|
||||
import { SpecVersionsLoader } from '../../components/SpecVersionsLoader';
|
||||
import { SpecVersionsProvider } from '../../hooks/useSpecVersions';
|
||||
import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo';
|
||||
import { AuthFlowsLoader } from '../../components/AuthFlowsLoader';
|
||||
import { AuthFlowsProvider } from '../../hooks/useAuthFlows';
|
||||
import { AuthServerProvider } from '../../hooks/useAuthServer';
|
||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||
import { AuthLayout } from '../../components/auth-layout';
|
||||
import { useUnauthThemeManager } from '../ThemeManager';
|
||||
|
||||
const currentAuthPath = (pathname: string): string => {
|
||||
if (matchPath(LOGIN_PATH, pathname)) {
|
||||
return LOGIN_PATH;
|
||||
}
|
||||
if (matchPath(RESET_PASSWORD_PATH, pathname)) {
|
||||
return RESET_PASSWORD_PATH;
|
||||
}
|
||||
if (matchPath(REGISTER_PATH, pathname)) {
|
||||
return REGISTER_PATH;
|
||||
}
|
||||
return LOGIN_PATH;
|
||||
};
|
||||
|
||||
function AuthLayoutLoading({ message }: { message: string }) {
|
||||
return (
|
||||
<Box justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Spinner size="100" variant="Secondary" />
|
||||
<Text align="Center" size="T300">
|
||||
{message}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthLayoutError({ message }: { message: string }) {
|
||||
return (
|
||||
<Box justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Text align="Center" style={{ color: color.Critical.Main }} size="T300">
|
||||
{message}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthRoot() {
|
||||
useUnauthThemeManager();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { server: urlEncodedServer } = useParams();
|
||||
|
||||
const clientConfig = useClientConfig();
|
||||
|
||||
const defaultServer = clientDefaultServer(clientConfig);
|
||||
let server: string = urlEncodedServer ? tryDecodeURIComponent(urlEncodedServer) : defaultServer;
|
||||
|
||||
if (!clientAllowedServer(clientConfig, server)) {
|
||||
server = defaultServer;
|
||||
}
|
||||
|
||||
const [discoveryState, discoverServer] = useAsyncCallback(
|
||||
useCallback(async (serverName: string) => {
|
||||
const response = await autoDiscovery(fetch, serverName);
|
||||
return {
|
||||
serverName,
|
||||
response,
|
||||
};
|
||||
}, [])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (server) discoverServer(server);
|
||||
}, [discoverServer, server]);
|
||||
|
||||
// if server is mismatches with path server, update path
|
||||
useEffect(() => {
|
||||
if (!urlEncodedServer || tryDecodeURIComponent(urlEncodedServer) !== server) {
|
||||
navigate(
|
||||
generatePath(currentAuthPath(location.pathname), {
|
||||
server: encodeURIComponent(server),
|
||||
}),
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
}, [urlEncodedServer, navigate, location, server]);
|
||||
|
||||
const selectServer = useCallback(
|
||||
(newServer: string) => {
|
||||
if (newServer === server) {
|
||||
if (discoveryState.status === AsyncStatus.Loading) return;
|
||||
discoverServer(server);
|
||||
return;
|
||||
}
|
||||
navigate(
|
||||
generatePath(currentAuthPath(location.pathname), { server: encodeURIComponent(newServer) })
|
||||
);
|
||||
},
|
||||
[navigate, location, discoveryState, server, discoverServer]
|
||||
);
|
||||
|
||||
const [autoDiscoveryError, autoDiscoveryInfo] =
|
||||
discoveryState.status === AsyncStatus.Success ? discoveryState.data.response : [];
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="label" size="L400" priority="300">
|
||||
Homeserver
|
||||
</Text>
|
||||
<ServerPicker
|
||||
server={server}
|
||||
serverList={clientConfig.homeserverList ?? []}
|
||||
allowCustomServer={clientConfig.allowCustomHomeservers}
|
||||
onServerChange={selectServer}
|
||||
/>
|
||||
</Box>
|
||||
{discoveryState.status === AsyncStatus.Loading && (
|
||||
<AuthLayoutLoading message="Looking for homeserver..." />
|
||||
)}
|
||||
{discoveryState.status === AsyncStatus.Error && (
|
||||
<AuthLayoutError message="Failed to find homeserver." />
|
||||
)}
|
||||
{autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && (
|
||||
<AuthLayoutError
|
||||
message={`Failed to connect. Homeserver configuration found with ${autoDiscoveryError.host} appears unusable.`}
|
||||
/>
|
||||
)}
|
||||
{autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && (
|
||||
<AuthLayoutError message="Failed to connect. Homeserver configuration base_url appears invalid." />
|
||||
)}
|
||||
{discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && (
|
||||
<AuthServerProvider value={discoveryState.data.serverName}>
|
||||
<AutoDiscoveryInfoProvider value={autoDiscoveryInfo}>
|
||||
<SpecVersionsLoader
|
||||
baseUrl={autoDiscoveryInfo['m.homeserver'].base_url}
|
||||
fallback={() => (
|
||||
<AuthLayoutLoading
|
||||
message={`Connecting to ${autoDiscoveryInfo['m.homeserver'].base_url}`}
|
||||
/>
|
||||
)}
|
||||
error={() => (
|
||||
<AuthLayoutError message="Failed to connect. Either homeserver is unavailable at this moment or does not exist." />
|
||||
)}
|
||||
>
|
||||
{(specVersions) => (
|
||||
<SpecVersionsProvider value={specVersions}>
|
||||
<AuthFlowsLoader
|
||||
fallback={() => <AuthLayoutLoading message="Loading authentication flow..." />}
|
||||
error={() => (
|
||||
<AuthLayoutError message="Failed to get authentication flow information." />
|
||||
)}
|
||||
>
|
||||
{(authFlows) => (
|
||||
<AuthFlowsProvider value={authFlows}>
|
||||
<Outlet />
|
||||
</AuthFlowsProvider>
|
||||
)}
|
||||
</AuthFlowsLoader>
|
||||
</SpecVersionsProvider>
|
||||
)}
|
||||
</SpecVersionsLoader>
|
||||
</AutoDiscoveryInfoProvider>
|
||||
</AuthServerProvider>
|
||||
)}
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export * from './AuthLayout';
|
||||
export * from './AuthRoot';
|
||||
export * from './login';
|
||||
export * from './register';
|
||||
export * from './reset-password';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows';
|
|||
import { PasswordLoginForm } from './PasswordLoginForm';
|
||||
import { SSOLogin } from '../SSOLogin';
|
||||
import { TokenLogin } from './TokenLogin';
|
||||
import { OrDivider } from '../OrDivider';
|
||||
import { OrDivider } from '../../../components/OrDivider';
|
||||
import { getLoginPath, getRegisterPath, withSearchParam } from '../../pathUtils';
|
||||
import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin';
|
||||
import { LoginPathSearchParams } from '../../paths';
|
||||
|
|
@ -90,7 +90,7 @@ export function Login() {
|
|||
<span data-spacing-node />
|
||||
</>
|
||||
)}
|
||||
<Text align="Center">
|
||||
<Text size="T300" align="Center">
|
||||
Do not have an account? <Link to={getRegisterPath(server)}>Register</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import {
|
|||
config,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix';
|
||||
import { EMAIL_REGEX } from '../../../utils/regex';
|
||||
import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
|
||||
|
|
@ -28,9 +29,11 @@ import { useClientConfig } from '../../../hooks/useClientConfig';
|
|||
import { LoginError, factoryGetBaseUrl, login, useLoginComplete } from './loginUtil';
|
||||
import { PasswordInput } from '../../../components/password-input';
|
||||
import { FieldError } from '../FiledError';
|
||||
import { getResetPasswordPath } from '../../pathUtils';
|
||||
import { getHomePath, getResetPasswordPath } from '../../pathUtils';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { getInitialDeviceDisplayName } from '../../../utils/user-agent';
|
||||
import { sessionUserIdAtom } from '../../../state/sessions';
|
||||
import { userHasSession } from '../../../../client/sessions';
|
||||
|
||||
function UsernameHint({ server }: { server: string }) {
|
||||
const [anchor, setAnchor] = useState<RectCords>();
|
||||
|
|
@ -108,6 +111,9 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
|
|||
const server = useAuthServer();
|
||||
const clientConfig = useClientConfig();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const setSessionUserId = useSetAtom(sessionUserIdAtom);
|
||||
|
||||
const serverDiscovery = useAutoDiscoveryInfo();
|
||||
const baseUrl = serverDiscovery['m.homeserver'].base_url;
|
||||
|
||||
|
|
@ -115,9 +121,20 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
|
|||
const [loginError, customLoginResponse] =
|
||||
loginState.status === AsyncStatus.Success ? loginState.data : [];
|
||||
|
||||
useLoginComplete(customLoginResponse);
|
||||
const handleLoginComplete = useLoginComplete();
|
||||
|
||||
const openExisting = (userId: string) => {
|
||||
setSessionUserId(userId);
|
||||
navigate(getHomePath(), { replace: true });
|
||||
};
|
||||
|
||||
const handleUsernameLogin = (username: string, password: string) => {
|
||||
const userId = `@${username}:${server}`;
|
||||
if (userHasSession(userId)) {
|
||||
openExisting(userId);
|
||||
return;
|
||||
}
|
||||
|
||||
startLogin(baseUrl, {
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
|
|
@ -126,10 +143,17 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
|
|||
},
|
||||
password,
|
||||
initial_device_display_name: getInitialDeviceDisplayName(),
|
||||
}).then(([, data]) => {
|
||||
if (data) handleLoginComplete(data);
|
||||
});
|
||||
};
|
||||
|
||||
const handleMxIdLogin = async (mxId: string, password: string) => {
|
||||
if (userHasSession(mxId)) {
|
||||
openExisting(mxId);
|
||||
return;
|
||||
}
|
||||
|
||||
const mxIdServer = getMxIdServer(mxId);
|
||||
const mxIdUsername = getMxIdLocalPart(mxId);
|
||||
if (!mxIdServer || !mxIdUsername) return;
|
||||
|
|
@ -144,6 +168,8 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
|
|||
},
|
||||
password,
|
||||
initial_device_display_name: getInitialDeviceDisplayName(),
|
||||
}).then(([, data]) => {
|
||||
if (data) handleLoginComplete(data);
|
||||
});
|
||||
};
|
||||
const handleEmailLogin = (email: string, password: string) => {
|
||||
|
|
@ -156,6 +182,8 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
|
|||
},
|
||||
password,
|
||||
initial_device_display_name: getInitialDeviceDisplayName(),
|
||||
}).then(([, data]) => {
|
||||
if (data) handleLoginComplete(data);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -49,18 +49,19 @@ export function TokenLogin({ token }: TokenLoginProps) {
|
|||
|
||||
const [loginState, startLogin] = useAsyncCallback(useCallback(login, []));
|
||||
|
||||
const handleLoginComplete = useLoginComplete();
|
||||
|
||||
useEffect(() => {
|
||||
startLogin(baseUrl, {
|
||||
type: 'm.login.token',
|
||||
token,
|
||||
initial_device_display_name: getInitialDeviceDisplayName(),
|
||||
}).then(([, data]) => {
|
||||
if (data) handleLoginComplete(data);
|
||||
});
|
||||
}, [baseUrl, token, startLogin]);
|
||||
}, [baseUrl, token, startLogin, handleLoginComplete]);
|
||||
|
||||
const [loginError, customLoginResponse] =
|
||||
loginState.status === AsyncStatus.Success ? loginState.data : [];
|
||||
|
||||
useLoginComplete(customLoginResponse);
|
||||
const [loginError] = loginState.status === AsyncStatus.Success ? loginState.data : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import to from 'await-to-js';
|
||||
import { useEffect } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { postLogin, PostLoginBody, PostLoginOK } from 'neurone/lib/api/matrix/client/v3/login';
|
||||
import { parseMatrixResp } from 'neurone/lib/matrix-request';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { postLogout } from 'neurone/lib/api/matrix/client/v3/logout';
|
||||
import { ClientConfig, clientAllowedServer } from '../../../hooks/useClientConfig';
|
||||
import { autoDiscovery, specVersions } from '../../../cs-api';
|
||||
import { ErrorCode } from '../../../cs-errorcode';
|
||||
|
|
@ -11,8 +13,9 @@ import {
|
|||
getAfterLoginRedirectPath,
|
||||
} from '../../afterLoginRedirectPath';
|
||||
import { getHomePath } from '../../pathUtils';
|
||||
import { setFallbackSession } from '../../../state/sessions';
|
||||
import { getJSONRequest } from '../../../../client/request';
|
||||
import { getAuthJSONRequest, getJSONRequest } from '../../../../client/request';
|
||||
import { sessionsAtom, sessionUserIdAtom } from '../../../state/sessions';
|
||||
import { userHasSession } from '../../../../client/sessions';
|
||||
|
||||
export enum GetBaseUrlError {
|
||||
NotAllow = 'NotAllow',
|
||||
|
|
@ -106,16 +109,43 @@ export const login = async (
|
|||
];
|
||||
};
|
||||
|
||||
export const useLoginComplete = (data?: CustomLoginResponse) => {
|
||||
export const useLoginComplete = () => {
|
||||
const navigate = useNavigate();
|
||||
const setSession = useSetAtom(sessionsAtom);
|
||||
const setSessionUserId = useSetAtom(sessionUserIdAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const handleLoginComplete = useCallback(
|
||||
(data: CustomLoginResponse) => {
|
||||
const { response: loginRes, baseUrl: loginBaseUrl } = data;
|
||||
setFallbackSession(loginRes.access_token, loginRes.device_id, loginRes.user_id, loginBaseUrl);
|
||||
|
||||
setSessionUserId(loginRes.user_id);
|
||||
|
||||
if (userHasSession(loginRes.user_id)) {
|
||||
// user login with session that already exists.
|
||||
// this could happen if user login via SSO or OAuth.
|
||||
// open existing session and logout this request.
|
||||
const request = getAuthJSONRequest(loginBaseUrl, loginRes.access_token);
|
||||
request(postLogout({}));
|
||||
} else {
|
||||
setSession({
|
||||
type: 'PUT',
|
||||
session: {
|
||||
accessToken: loginRes.access_token,
|
||||
deviceId: loginRes.device_id,
|
||||
userId: loginRes.user_id,
|
||||
baseUrl: loginBaseUrl,
|
||||
expiresInMs: loginRes.expires_in_ms,
|
||||
refreshToken: loginRes.refresh_token,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const afterLoginRedirectUrl = getAfterLoginRedirectPath();
|
||||
deleteAfterLoginRedirectPath();
|
||||
navigate(afterLoginRedirectUrl ?? getHomePath(), { replace: true });
|
||||
}
|
||||
}, [data, navigate]);
|
||||
},
|
||||
[navigate, setSession, setSessionUserId]
|
||||
);
|
||||
|
||||
return handleLoginComplete;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -197,13 +197,13 @@ export function PasswordRegisterForm({
|
|||
Error,
|
||||
[PostRegisterBody]
|
||||
>(useCallback(async (registerReqData) => register(baseUrl, registerReqData), [baseUrl]));
|
||||
const [ongoingAuthData, registerError, customRegisterResp] =
|
||||
const [ongoingAuthData, registerError] =
|
||||
registerState.status === AsyncStatus.Success ? registerState.data : [];
|
||||
|
||||
const registerRequestError =
|
||||
registerState.status === AsyncStatus.Error ? registerState.error : undefined;
|
||||
|
||||
useRegisterComplete(customRegisterResp);
|
||||
const handleRegisterComplete = useRegisterComplete();
|
||||
|
||||
const handleSubmit: ChangeEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
|
@ -250,6 +250,9 @@ export function PasswordRegisterForm({
|
|||
session: authData.session,
|
||||
},
|
||||
initial_device_display_name: getInitialDeviceDisplayName(),
|
||||
}).then((result) => {
|
||||
const [, , data] = result;
|
||||
if (data) handleRegisterComplete(data);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useAuthServer } from '../../../hooks/useAuthServer';
|
|||
import { RegisterFlowStatus, useAuthFlows } from '../../../hooks/useAuthFlows';
|
||||
import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows';
|
||||
import { PasswordRegisterForm, SUPPORTED_REGISTER_STAGES } from '../register/PasswordRegisterForm';
|
||||
import { OrDivider } from '../OrDivider';
|
||||
import { OrDivider } from '../../../components/OrDivider';
|
||||
import { SSOLogin } from '../SSOLogin';
|
||||
import { SupportedUIAFlowsLoader } from '../../../components/SupportedUIAFlowsLoader';
|
||||
import { getLoginPath } from '../../pathUtils';
|
||||
|
|
@ -89,7 +89,7 @@ export function Register() {
|
|||
<span data-spacing-node />
|
||||
</>
|
||||
)}
|
||||
<Text align="Center">
|
||||
<Text size="T300" align="Center">
|
||||
Already have an account? <Link to={getLoginPath(server)}>Login</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import {
|
|||
PostRegisterOK,
|
||||
} from 'neurone/lib/api/matrix/client/v3/register';
|
||||
import { parseMatrixResp } from 'neurone/lib/matrix-request';
|
||||
import { useEffect } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { LoginPathSearchParams } from '../../paths';
|
||||
import { ErrorCode } from '../../../cs-errorcode';
|
||||
import {
|
||||
|
|
@ -14,9 +15,9 @@ import {
|
|||
} from '../../afterLoginRedirectPath';
|
||||
import { getHomePath, getLoginPath, withSearchParam } from '../../pathUtils';
|
||||
import { getMxIdLocalPart, getMxIdServer } from '../../../utils/matrix';
|
||||
import { setFallbackSession } from '../../../state/sessions';
|
||||
import { AuthResponse } from '../../../utils/matrix-uia';
|
||||
import { getJSONRequest } from '../../../../client/request';
|
||||
import { sessionsAtom, sessionUserIdAtom } from '../../../state/sessions';
|
||||
|
||||
export enum RegisterError {
|
||||
UserTaken = 'UserTaken',
|
||||
|
|
@ -136,11 +137,13 @@ export const register = async (
|
|||
];
|
||||
};
|
||||
|
||||
export const useRegisterComplete = (data?: CustomRegisterResponse) => {
|
||||
export const useRegisterComplete = () => {
|
||||
const navigate = useNavigate();
|
||||
const setSession = useSetAtom(sessionsAtom);
|
||||
const setSessionUserId = useSetAtom(sessionUserIdAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const handleRegisterComplete = useCallback(
|
||||
(data: CustomRegisterResponse) => {
|
||||
const { response, baseUrl } = data;
|
||||
|
||||
const userId = response.user_id;
|
||||
|
|
@ -148,7 +151,19 @@ export const useRegisterComplete = (data?: CustomRegisterResponse) => {
|
|||
const deviceId = response.device_id;
|
||||
|
||||
if (accessToken && deviceId) {
|
||||
setFallbackSession(accessToken, deviceId, userId, baseUrl);
|
||||
setSessionUserId(userId);
|
||||
setSession({
|
||||
type: 'PUT',
|
||||
session: {
|
||||
accessToken,
|
||||
deviceId,
|
||||
userId,
|
||||
baseUrl,
|
||||
expiresInMs: response.expires_in_ms,
|
||||
refreshToken: response.refresh_token,
|
||||
},
|
||||
});
|
||||
|
||||
const afterLoginRedirectPath = getAfterLoginRedirectPath();
|
||||
deleteAfterLoginRedirectPath();
|
||||
navigate(afterLoginRedirectPath ?? getHomePath(), { replace: true });
|
||||
|
|
@ -162,6 +177,9 @@ export const useRegisterComplete = (data?: CustomRegisterResponse) => {
|
|||
{ replace: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [data, navigate]);
|
||||
},
|
||||
[navigate, setSession, setSessionUserId]
|
||||
);
|
||||
|
||||
return handleRegisterComplete;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,16 +7,17 @@ import { makeNavToActivePathAtom } from '../../state/navToActivePath';
|
|||
import { NavToActivePathProvider } from '../../state/hooks/navToActivePath';
|
||||
import { makeOpenedSidebarFolderAtom } from '../../state/openedSidebarFolder';
|
||||
import { OpenedSidebarFolderProvider } from '../../state/hooks/openedSidebarFolder';
|
||||
import { useClient } from '../../hooks/useClient';
|
||||
import { makeCallPreferencesAtom } from '../../state/callPreferences';
|
||||
import { CallPreferencesProvider } from '../../state/hooks/callPreferences';
|
||||
import { makeSettingsAtom } from '../../state/settings';
|
||||
import { SettingsAtomProvider } from '../../state/hooks/settings';
|
||||
import { useSession } from '../../hooks/useSessions';
|
||||
|
||||
type ClientInitStorageAtomProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
export function ClientInitStorageAtom({ children }: ClientInitStorageAtomProps) {
|
||||
const ct = useClient();
|
||||
const { userId } = ct.session;
|
||||
const { userId } = useSession();
|
||||
|
||||
const closedNavCategoriesAtom = useMemo(() => makeClosedNavCategoriesAtom(userId), [userId]);
|
||||
|
||||
|
|
@ -28,13 +29,15 @@ export function ClientInitStorageAtom({ children }: ClientInitStorageAtomProps)
|
|||
|
||||
const callPreferencesAtom = useMemo(() => makeCallPreferencesAtom(userId), [userId]);
|
||||
|
||||
const settingsAtom = useMemo(() => makeSettingsAtom(userId), [userId]);
|
||||
|
||||
return (
|
||||
<ClosedNavCategoriesProvider value={closedNavCategoriesAtom}>
|
||||
<ClosedLobbyCategoriesProvider value={closedLobbyCategoriesAtom}>
|
||||
<NavToActivePathProvider value={navToActivePathAtom}>
|
||||
<OpenedSidebarFolderProvider value={openedSidebarFolderAtom}>
|
||||
<CallPreferencesProvider value={callPreferencesAtom}>
|
||||
{children}
|
||||
<SettingsAtomProvider value={settingsAtom}>{children}</SettingsAtomProvider>
|
||||
</CallPreferencesProvider>
|
||||
</OpenedSidebarFolderProvider>
|
||||
</NavToActivePathProvider>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import React from 'react';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
|
||||
function SystemEmojiFeature() {
|
||||
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||
const [twitterEmoji] = useSetting('twitterEmoji');
|
||||
|
||||
if (twitterEmoji) {
|
||||
document.documentElement.style.setProperty('--font-emoji', 'Twemoji');
|
||||
|
|
@ -15,7 +14,7 @@ function SystemEmojiFeature() {
|
|||
}
|
||||
|
||||
function PageZoomFeature() {
|
||||
const [pageZoom] = useSetting(settingsAtom, 'pageZoom');
|
||||
const [pageZoom] = useSetting('pageZoom');
|
||||
|
||||
if (pageZoom === 100) {
|
||||
document.documentElement.style.removeProperty('font-size');
|
||||
|
|
|
|||
|
|
@ -8,29 +8,22 @@ import { SpecVersions } from './SpecVersions';
|
|||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { SyncStatus } from './SyncStatus';
|
||||
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
|
||||
import { getFallbackSession } from '../../state/sessions';
|
||||
import { initializeClient } from '../../../client/initialize';
|
||||
import { ClientContext } from '../../../client/context';
|
||||
import { initializeClient, InitializeClientResult } from '../../../client/initialize';
|
||||
import { ClientContextProvider } from '../../hooks/useClient';
|
||||
import { InitialSyncSplashScreen } from './InitialSyncSplashScreen';
|
||||
import { AutoDiscovery } from './AutoDiscovery';
|
||||
import { ClientRootOptions } from './ClientRootOptions';
|
||||
import { FlickerText } from '../../styles/Animations.css';
|
||||
import { useSession } from '../../hooks/useSessions';
|
||||
|
||||
type ClientRootProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
export function ClientRoot({ children }: ClientRootProps) {
|
||||
const { baseUrl, userId } = getFallbackSession() ?? {};
|
||||
const session = useSession();
|
||||
|
||||
const [initState, initialize] = useAsyncCallback<ClientContext, Error, []>(
|
||||
useCallback(() => {
|
||||
const session = getFallbackSession();
|
||||
if (!session) {
|
||||
throw new Error('No session Found!');
|
||||
}
|
||||
return initializeClient(session);
|
||||
}, [])
|
||||
const [initState, initialize] = useAsyncCallback<InitializeClientResult, Error, []>(
|
||||
useCallback(() => initializeClient(session), [session])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -40,16 +33,18 @@ export function ClientRoot({ children }: ClientRootProps) {
|
|||
}, [initState, initialize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initState.status === AsyncStatus.Success) {
|
||||
const ct = initState.data;
|
||||
ct.sync.controller.start();
|
||||
}
|
||||
const { ct, dispose } = initState.status === AsyncStatus.Success ? initState.data : {};
|
||||
ct?.sync.controller.start();
|
||||
|
||||
return () => {
|
||||
dispose?.();
|
||||
};
|
||||
}, [initState]);
|
||||
|
||||
return (
|
||||
<AutoDiscovery userId={userId!} baseUrl={baseUrl!}>
|
||||
{initState.status !== AsyncStatus.Success && <ClientRootOptions userId={userId!} />}
|
||||
<SpecVersions baseUrl={baseUrl!}>
|
||||
<AutoDiscovery userId={session.userId} baseUrl={session.baseUrl}>
|
||||
{initState.status !== AsyncStatus.Success && <ClientRootOptions />}
|
||||
<SpecVersions baseUrl={session.baseUrl}>
|
||||
{initState.status === AsyncStatus.Error && (
|
||||
<SplashScreen>
|
||||
<Box
|
||||
|
|
@ -98,7 +93,7 @@ export function ClientRoot({ children }: ClientRootProps) {
|
|||
/>
|
||||
)}
|
||||
{initState.status === AsyncStatus.Success && (
|
||||
<ClientContextProvider value={initState.data}>
|
||||
<ClientContextProvider value={initState.data.ct}>
|
||||
<InitialSyncSplashScreen>
|
||||
<SyncStatus />
|
||||
<ServerConfigsLoader>
|
||||
|
|
|
|||
|
|
@ -17,8 +17,12 @@ import { stopPropagation } from '../../utils/keyboard';
|
|||
import { deleteClientStorage } from '../../../client/storage';
|
||||
import { clearNavToActivePathStore } from '../../state/navToActivePath';
|
||||
import { deleteClientCryptoStorage } from '../../../client/crypto';
|
||||
import { useSession } from '../../hooks/useSessions';
|
||||
import { clearUserLocalStorage } from '../../../client/localStorage';
|
||||
import { removeSession } from '../../../client/sessions';
|
||||
|
||||
export function ClientRootOptions({ userId, ct }: { userId: string; ct?: ClientContext }) {
|
||||
export function ClientRootOptions({ ct }: { ct?: ClientContext }) {
|
||||
const session = useSession();
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
|
||||
const handleToggle: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
|
|
@ -29,24 +33,27 @@ export function ClientRootOptions({ userId, ct }: { userId: string; ct?: ClientC
|
|||
});
|
||||
};
|
||||
|
||||
const handleClearCache = () => {
|
||||
clearNavToActivePathStore(userId);
|
||||
const handleClearCache = async () => {
|
||||
clearNavToActivePathStore(session.userId);
|
||||
if (ct) {
|
||||
ct.clearCacheAndReload();
|
||||
return;
|
||||
}
|
||||
deleteClientStorage(userId);
|
||||
await deleteClientStorage(session);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
if (ct) {
|
||||
ct.logout();
|
||||
await ct.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.clear();
|
||||
await deleteClientCryptoStorage(userId);
|
||||
await deleteClientStorage(userId);
|
||||
await deleteClientCryptoStorage(session);
|
||||
await deleteClientStorage(session);
|
||||
clearUserLocalStorage(session.userId);
|
||||
removeSession(session);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export function InitialSyncSplashScreen({ children }: { children: ReactNode }) {
|
|||
if (initialSync) {
|
||||
return (
|
||||
<>
|
||||
<ClientRootOptions userId={ct.session.userId} />
|
||||
<ClientRootOptions ct={ct} />
|
||||
<InitialSyncProgressScreen sync={ct.sync} progressObservable={initialSync.progress} />;
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCatego
|
|||
import { markAsRead } from '../../../utils/notifications';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import {
|
||||
getRoomNotificationMode,
|
||||
useRoomsNotificationPreferencesContext,
|
||||
|
|
@ -66,7 +65,7 @@ type DirectMenuProps = {
|
|||
requestClose: () => void;
|
||||
};
|
||||
const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }, ref) => {
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
const directRooms = useDirectRooms();
|
||||
const roomsNotifications = useRoomsNotifications(directRooms, roomToNotificationsAtom);
|
||||
const notifications = useRoomsTotalNotifications(roomsNotifications);
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page
|
|||
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import {
|
||||
getRoomNotificationMode,
|
||||
useRoomsNotificationPreferencesContext,
|
||||
|
|
@ -79,7 +78,7 @@ type HomeMenuProps = {
|
|||
};
|
||||
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, ref) => {
|
||||
const orphanRooms = useHomeRooms();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
const roomsNotifications = useRoomsNotifications(orphanRooms, roomToNotificationsAtom);
|
||||
const notifications = useRoomsTotalNotifications(roomsNotifications);
|
||||
|
||||
|
|
|
|||
|
|
@ -49,10 +49,9 @@ import { allRoomsAtom } from '../../../state/room-list/roomList';
|
|||
import { getIgnoredUsersContent, useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
||||
import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { InviteData, useInviteRooms } from '../../../hooks/storage/useInviteRooms';
|
||||
import { useClient } from '../../../hooks/useClient';
|
||||
import { getJoinRoomMemberId, JoinRoomMemberObject } from '../../../../client/storage/collections';
|
||||
import { getJoinRoomMemberId } from '../../../../client/storage/collections';
|
||||
import { useObservableValuePromise } from '../../../hooks/useObservableValue';
|
||||
import { Membership } from '../../../../types/matrix/room';
|
||||
import { AccountDataType } from '../../../../types/matrix/accountData';
|
||||
|
|
@ -708,8 +707,8 @@ export function Invites() {
|
|||
);
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
const [dateFormatString] = useSetting('dateFormatString');
|
||||
|
||||
const handleNavigate = (roomId: string, space: boolean) => {
|
||||
if (space) {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ import {
|
|||
} from '../../../plugins/react-custom-html-parser';
|
||||
import { RenderMessageContent } from '../../../components/RenderMessageContent';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { Image } from '../../../components/media';
|
||||
import { ImageViewer } from '../../../components/image-viewer';
|
||||
import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
||||
|
|
@ -561,12 +560,12 @@ const DEFAULT_REFRESH_MS = 7000;
|
|||
|
||||
export function Notifications() {
|
||||
const mx = useMatrixClient();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
const [mediaAutoLoad] = useSetting('mediaAutoLoad');
|
||||
const [urlPreview] = useSetting('urlPreview');
|
||||
const [legacyUsernameColor] = useSetting('legacyUsernameColor');
|
||||
const [hour24Clock] = useSetting('hour24Clock');
|
||||
const [dateFormatString] = useSetting('dateFormatString');
|
||||
const screenSize = useScreenSizeContext();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
|
|||
import { useDirectRooms } from '../direct/useDirectRooms';
|
||||
import { markAsRead } from '../../../utils/notifications';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import {
|
||||
TotalNotifications,
|
||||
|
|
@ -39,7 +38,7 @@ type DirectMenuProps = {
|
|||
};
|
||||
const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(
|
||||
({ directRooms, roomToNotifications, notifications, requestClose }, ref) => {
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
directRooms.forEach((rId) => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { useHomeRooms } from '../home/useHomeRooms';
|
|||
import { markAsRead } from '../../../utils/notifications';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import {
|
||||
TotalNotifications,
|
||||
useRoomsNotifications,
|
||||
|
|
@ -39,7 +38,7 @@ type HomeMenuProps = {
|
|||
};
|
||||
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(
|
||||
({ homeRooms, roomToNotifications, notifications, requestClose }, ref) => {
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
homeRooms.forEach((rId) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,25 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Text } from 'folds';
|
||||
import React, { MouseEventHandler, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
config,
|
||||
Icon,
|
||||
Icons,
|
||||
Line,
|
||||
Menu,
|
||||
MenuItem,
|
||||
PopOut,
|
||||
RectCords,
|
||||
Scroll,
|
||||
Text,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { getMxIdLocalPart } from '../../../utils/matrix';
|
||||
import { getMxIdLocalPart, getMxIdServer } from '../../../utils/matrix';
|
||||
import { nameInitials } from '../../../utils/common';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { Settings } from '../../../features/settings';
|
||||
|
|
@ -10,6 +27,137 @@ import { useUserProfile } from '../../../hooks/useUserProfile';
|
|||
import { Modal500 } from '../../../components/Modal500';
|
||||
import { useClient } from '../../../hooks/useClient';
|
||||
import { mxcToHttp } from '../../../../client/mxcToHttp';
|
||||
import { getRootPath } from '../../pathUtils';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { useSession, useSessions } from '../../../hooks/useSessions';
|
||||
import { sessionUserIdAtom } from '../../../state/sessions';
|
||||
import { Session } from '../../../../client/sessions';
|
||||
|
||||
type AccountTileProps = {
|
||||
session: Session;
|
||||
};
|
||||
function AccountTile({ session }: AccountTileProps) {
|
||||
const { userId } = session;
|
||||
|
||||
const currentSession = useSession();
|
||||
const currentSelected = userId === currentSession.userId;
|
||||
|
||||
const setSessionUserId = useSetAtom(sessionUserIdAtom);
|
||||
|
||||
const profile = useUserProfile(userId);
|
||||
|
||||
const username = getMxIdLocalPart(userId) ?? userId;
|
||||
const server = getMxIdServer(userId) ?? userId;
|
||||
|
||||
const displayName = profile?.displayName ?? username;
|
||||
const avatarUrl = profile?.avatarUrl
|
||||
? mxcToHttp(session.baseUrl, profile.avatarUrl, true).thumbnail({
|
||||
width: 96,
|
||||
height: 96,
|
||||
method: 'crop',
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
size="400"
|
||||
radii="300"
|
||||
onClick={currentSelected ? undefined : () => setSessionUserId(userId)}
|
||||
variant={currentSelected ? 'Success' : 'Surface'}
|
||||
aria-pressed={currentSelected}
|
||||
before={
|
||||
<Avatar size="200" radii="Pill">
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
alt={userId}
|
||||
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
after={
|
||||
<Text style={{ maxWidth: toRem(100) }} as="span" size="L400" truncate>
|
||||
{server}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="B300" truncate>
|
||||
{displayName}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
type AccountSwitcherProps = {
|
||||
cords: RectCords;
|
||||
requestClose: () => void;
|
||||
};
|
||||
function AccountSwitcher({ cords, requestClose }: AccountSwitcherProps) {
|
||||
const navigate = useNavigate();
|
||||
const setSessionUserId = useSetAtom(sessionUserIdAtom);
|
||||
|
||||
const sessions = useSessions();
|
||||
|
||||
const handleManageAccounts = () => {
|
||||
setSessionUserId(undefined);
|
||||
navigate(getRootPath());
|
||||
};
|
||||
|
||||
return (
|
||||
<PopOut
|
||||
anchor={cords}
|
||||
position="Right"
|
||||
align="End"
|
||||
offset={6}
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
returnFocusOnDeactivate: false,
|
||||
onDeactivate: () => requestClose(),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ maxWidth: toRem(250), width: '100vw' }}>
|
||||
{sessions.length > 0 && (
|
||||
<>
|
||||
<Scroll style={{ maxHeight: toRem(300) }} size="300" hideTrack>
|
||||
<Box
|
||||
direction="Column"
|
||||
gap="0"
|
||||
style={{
|
||||
padding: `${config.space.S200} 0 ${config.space.S200} ${config.space.S200}`,
|
||||
}}
|
||||
>
|
||||
{sessions.map((session) => (
|
||||
<AccountTile key={session.userId} session={session} />
|
||||
))}
|
||||
</Box>
|
||||
</Scroll>
|
||||
<Line size="300" />
|
||||
</>
|
||||
)}
|
||||
<Box direction="Column" gap="0" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
size="300"
|
||||
radii="300"
|
||||
after={<Icon size="100" src={Icons.User} />}
|
||||
onClick={handleManageAccounts}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="B300" truncate>
|
||||
Manage Accounts
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsTab() {
|
||||
const ct = useClient();
|
||||
|
|
@ -28,14 +176,43 @@ export function SettingsTab() {
|
|||
})
|
||||
: undefined;
|
||||
|
||||
const openSettings = () => setSettings(true);
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
|
||||
const openSettings = () => {
|
||||
setMenuAnchor(undefined);
|
||||
setSettings(true);
|
||||
};
|
||||
const closeSettings = () => setSettings(false);
|
||||
|
||||
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
const cords = evt.currentTarget.getBoundingClientRect();
|
||||
setMenuAnchor((currentState) => {
|
||||
if (currentState) return undefined;
|
||||
return cords;
|
||||
});
|
||||
};
|
||||
|
||||
const [switcherAnchor, setSwitcherAnchor] = useState<RectCords>();
|
||||
|
||||
const handleOpenAccountSwitcher: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
const cords = evt.currentTarget.getBoundingClientRect();
|
||||
setSwitcherAnchor((currentState) => {
|
||||
if (currentState) return undefined;
|
||||
return cords;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarItem active={settings}>
|
||||
<SidebarItemTooltip tooltip="User Settings">
|
||||
<SidebarItemTooltip tooltip="User Menu">
|
||||
{(triggerRef) => (
|
||||
<SidebarAvatar as="button" ref={triggerRef} onClick={openSettings}>
|
||||
<SidebarAvatar
|
||||
as="button"
|
||||
ref={triggerRef}
|
||||
onClick={handleOpenMenu}
|
||||
onContextMenu={handleOpenAccountSwitcher}
|
||||
>
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
|
|
@ -44,11 +221,65 @@ export function SettingsTab() {
|
|||
</SidebarAvatar>
|
||||
)}
|
||||
</SidebarItemTooltip>
|
||||
<PopOut
|
||||
anchor={menuAnchor}
|
||||
position="Right"
|
||||
align="End"
|
||||
offset={6}
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
returnFocusOnDeactivate: false,
|
||||
onDeactivate: () => setMenuAnchor(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ maxWidth: toRem(200), width: '100vw' }}>
|
||||
<Box direction="Column" gap="0" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
size="300"
|
||||
radii="300"
|
||||
onClick={openSettings}
|
||||
after={<Icon size="100" src={Icons.Setting} />}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="B300" truncate>
|
||||
Settings
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
size="300"
|
||||
radii="300"
|
||||
after={<Icon size="100" src={Icons.ArrowUpDown} />}
|
||||
onClick={handleOpenAccountSwitcher}
|
||||
aria-pressed={!!switcherAnchor}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="B300" truncate>
|
||||
Switch Accounts
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
{settings && (
|
||||
<Modal500 requestClose={closeSettings}>
|
||||
<Settings requestClose={closeSettings} />
|
||||
</Modal500>
|
||||
)}
|
||||
{switcherAnchor && (
|
||||
<AccountSwitcher
|
||||
cords={switcherAnchor}
|
||||
requestClose={() => {
|
||||
setMenuAnchor(undefined);
|
||||
setSwitcherAnchor(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SidebarItem>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ import { getMatrixToRoom } from '../../../plugins/matrix-to';
|
|||
import { getViaServers } from '../../../plugins/via-servers';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
|
||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
|
||||
|
|
@ -115,7 +114,7 @@ type SpaceMenuProps = {
|
|||
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||
({ spaceId, requestClose, onUnpin, notifications }, ref) => {
|
||||
const ct = useClient();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
const powerLevelState = usePowerLevelsState(spaceId);
|
||||
const powerLevels = usePowerLevels(powerLevelState.value);
|
||||
const creators = useRoomCreators(spaceId);
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ import { stopPropagation } from '../../../utils/keyboard';
|
|||
import { getMatrixToRoom } from '../../../plugins/matrix-to';
|
||||
import { getViaServers } from '../../../plugins/via-servers';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import {
|
||||
getRoomNotificationMode,
|
||||
useRoomsNotificationPreferencesContext,
|
||||
|
|
@ -101,7 +100,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ requestClose },
|
|||
const ct = useClient();
|
||||
const { userId } = ct.session;
|
||||
const spaceId = useSpace();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const powerLevelsStateResult = usePowerLevelsState(spaceId);
|
||||
const powerLevels = usePowerLevels(powerLevelsStateResult.value);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { WritableAtom } from 'jotai';
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
setLocalStorageItem,
|
||||
} from './utils/atomWithLocalStorage';
|
||||
import { atomWithLocalStorage } from './utils/atomWithLocalStorage';
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '../../client/localStorage';
|
||||
|
||||
export type CallPreferences = {
|
||||
microphone: boolean;
|
||||
|
|
@ -19,10 +16,12 @@ const DEFAULT_PREFERENCES: CallPreferences = {
|
|||
sound: true,
|
||||
};
|
||||
|
||||
export type CallPreferencesAtom = WritableAtom<CallPreferences, [CallPreferences], undefined>;
|
||||
export type CallPreferencesAtom = WritableAtom<CallPreferences, [CallPreferences], void>;
|
||||
|
||||
const getStoreKey = (userId: string) => `${CALL_PREFERENCES}${userId}`;
|
||||
|
||||
export const makeCallPreferencesAtom = (userId: string): CallPreferencesAtom => {
|
||||
const storeKey = `${CALL_PREFERENCES}${userId}`;
|
||||
const storeKey = getStoreKey(userId);
|
||||
|
||||
const callPreferencesAtom = atomWithLocalStorage<CallPreferences>(
|
||||
storeKey,
|
||||
|
|
@ -37,3 +36,7 @@ export const makeCallPreferencesAtom = (userId: string): CallPreferencesAtom =>
|
|||
|
||||
return callPreferencesAtom;
|
||||
};
|
||||
|
||||
export const clearCallPreferenceStore = (userId: string) => {
|
||||
localStorage.removeItem(getStoreKey(userId));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { WritableAtom, atom } from 'jotai';
|
||||
import produce from 'immer';
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
setLocalStorageItem,
|
||||
} from './utils/atomWithLocalStorage';
|
||||
import { atomWithLocalStorage } from './utils/atomWithLocalStorage';
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '../../client/localStorage';
|
||||
|
||||
const CLOSED_LOBBY_CATEGORY = 'closedLobbyCategories';
|
||||
|
||||
|
|
@ -24,8 +21,10 @@ export type ClosedLobbyCategoriesAtom = WritableAtom<
|
|||
undefined
|
||||
>;
|
||||
|
||||
const getStoreKey = (userId: string) => `${CLOSED_LOBBY_CATEGORY}${userId}`;
|
||||
|
||||
export const makeClosedLobbyCategoriesAtom = (userId: string): ClosedLobbyCategoriesAtom => {
|
||||
const storeKey = `${CLOSED_LOBBY_CATEGORY}${userId}`;
|
||||
const storeKey = getStoreKey(userId);
|
||||
|
||||
const baseClosedLobbyCategoriesAtom = atomWithLocalStorage<Set<string>>(
|
||||
storeKey,
|
||||
|
|
@ -66,3 +65,7 @@ export const makeClosedLobbyCategoriesAtom = (userId: string): ClosedLobbyCatego
|
|||
};
|
||||
|
||||
export const makeLobbyCategoryId = (...args: string[]): string => args.join('|');
|
||||
|
||||
export const clearClosedLobbyCategoriesStore = (userId: string) => {
|
||||
localStorage.removeItem(getStoreKey(userId));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { WritableAtom, atom } from 'jotai';
|
||||
import produce from 'immer';
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
setLocalStorageItem,
|
||||
} from './utils/atomWithLocalStorage';
|
||||
import { atomWithLocalStorage } from './utils/atomWithLocalStorage';
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '../../client/localStorage';
|
||||
|
||||
const CLOSED_NAV_CATEGORY = 'closedNavCategories';
|
||||
|
||||
|
|
@ -24,8 +21,10 @@ export type ClosedNavCategoriesAtom = WritableAtom<
|
|||
undefined
|
||||
>;
|
||||
|
||||
const getStoreKey = (userId: string) => `${CLOSED_NAV_CATEGORY}${userId}`;
|
||||
|
||||
export const makeClosedNavCategoriesAtom = (userId: string): ClosedNavCategoriesAtom => {
|
||||
const storeKey = `${CLOSED_NAV_CATEGORY}${userId}`;
|
||||
const storeKey = getStoreKey(userId);
|
||||
|
||||
const baseClosedNavCategoriesAtom = atomWithLocalStorage<Set<string>>(
|
||||
storeKey,
|
||||
|
|
@ -66,3 +65,7 @@ export const makeClosedNavCategoriesAtom = (userId: string): ClosedNavCategories
|
|||
};
|
||||
|
||||
export const makeNavCategoryId = (...args: string[]): string => args.join('|');
|
||||
|
||||
export const clearClosedNavCategoriesStore = (userId: string) => {
|
||||
localStorage.removeItem(getStoreKey(userId));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,34 @@
|
|||
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import { useMemo } from 'react';
|
||||
import { Settings, settingsAtom as sAtom } from '../settings';
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
import { Settings, SettingsAtom } from '../settings';
|
||||
|
||||
const SettingsAtomContext = createContext<SettingsAtom | null>(null);
|
||||
|
||||
export const SettingsAtomProvider = SettingsAtomContext.Provider;
|
||||
export const useSettingsAtom = () => {
|
||||
const settingsAtom = useContext(SettingsAtomContext);
|
||||
|
||||
if (!settingsAtom) {
|
||||
throw new Error('Settings atom not provided!');
|
||||
}
|
||||
|
||||
return settingsAtom;
|
||||
};
|
||||
|
||||
export const useSettings = () => {
|
||||
const settingsAtom = useSettingsAtom();
|
||||
|
||||
return useAtomValue(settingsAtom);
|
||||
};
|
||||
|
||||
export type SettingSetter<K extends keyof Settings> =
|
||||
| Settings[K]
|
||||
| ((s: Settings[K]) => Settings[K]);
|
||||
|
||||
export const useSetSetting = <K extends keyof Settings>(settingsAtom: typeof sAtom, key: K) => {
|
||||
export const useSetSetting = <K extends keyof Settings>(key: K) => {
|
||||
const settingsAtom = useSettingsAtom();
|
||||
|
||||
const setterAtom = useMemo(
|
||||
() =>
|
||||
atom<null, [SettingSetter<K>], undefined>(null, (get, set, value) => {
|
||||
|
|
@ -22,12 +43,15 @@ export const useSetSetting = <K extends keyof Settings>(settingsAtom: typeof sAt
|
|||
};
|
||||
|
||||
export const useSetting = <K extends keyof Settings>(
|
||||
settingsAtom: typeof sAtom,
|
||||
key: K
|
||||
): [Settings[K], ReturnType<typeof useSetSetting<K>>] => {
|
||||
const settingsAtom = useSettingsAtom();
|
||||
|
||||
const selector = useMemo(() => (s: Settings) => s[key], [key]);
|
||||
|
||||
const setting = useAtomValue(selectAtom(settingsAtom, selector));
|
||||
|
||||
const setter = useSetSetting(settingsAtom, key);
|
||||
const setter = useSetSetting(key);
|
||||
|
||||
return [setting, setter];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import { WritableAtom, atom } from 'jotai';
|
||||
import produce from 'immer';
|
||||
import { Path } from 'react-router-dom';
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
setLocalStorageItem,
|
||||
} from './utils/atomWithLocalStorage';
|
||||
import { atomWithLocalStorage } from './utils/atomWithLocalStorage';
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '../../client/localStorage';
|
||||
|
||||
const NAV_TO_ACTIVE_PATH = 'navToActivePath';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { WritableAtom, atom } from 'jotai';
|
||||
import produce from 'immer';
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
setLocalStorageItem,
|
||||
} from './utils/atomWithLocalStorage';
|
||||
import { atomWithLocalStorage } from './utils/atomWithLocalStorage';
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '../../client/localStorage';
|
||||
|
||||
const OPENED_SIDEBAR_FOLDER = 'openedSidebarFolder';
|
||||
|
||||
|
|
@ -24,8 +21,10 @@ export type OpenedSidebarFolderAtom = WritableAtom<
|
|||
undefined
|
||||
>;
|
||||
|
||||
const getStoreKey = (userId: string) => `${OPENED_SIDEBAR_FOLDER}${userId}`;
|
||||
|
||||
export const makeOpenedSidebarFolderAtom = (userId: string): OpenedSidebarFolderAtom => {
|
||||
const storeKey = `${OPENED_SIDEBAR_FOLDER}${userId}`;
|
||||
const storeKey = getStoreKey(userId);
|
||||
|
||||
const baseOpenedSidebarFolderAtom = atomWithLocalStorage<Set<string>>(
|
||||
storeKey,
|
||||
|
|
@ -64,3 +63,7 @@ export const makeOpenedSidebarFolderAtom = (userId: string): OpenedSidebarFolder
|
|||
|
||||
return openedSidebarFolderAtom;
|
||||
};
|
||||
|
||||
export const clearOpenedSidebarFolderStore = (userId: string) => {
|
||||
localStorage.removeItem(getStoreKey(userId));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,140 +1,69 @@
|
|||
// import { atom } from 'jotai';
|
||||
// import {
|
||||
// atomWithLocalStorage,
|
||||
// getLocalStorageItem,
|
||||
// setLocalStorageItem,
|
||||
// } from './utils/atomWithLocalStorage';
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithLocalStorage } from './utils/atomWithLocalStorage';
|
||||
import {
|
||||
CLIENT_SESSIONS_STORE_KEY,
|
||||
getFallbackSession,
|
||||
removeFallbackSession,
|
||||
Session,
|
||||
Sessions,
|
||||
} from '../../client/sessions';
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '../../client/localStorage';
|
||||
|
||||
export type Session = {
|
||||
baseUrl: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
accessToken: string;
|
||||
expiresInMs?: number;
|
||||
refreshToken?: string;
|
||||
fallbackSdkStores?: boolean;
|
||||
};
|
||||
const baseSessionsAtom = atomWithLocalStorage<Sessions>(
|
||||
CLIENT_SESSIONS_STORE_KEY,
|
||||
(key) => {
|
||||
const defaultSessions: Sessions = [];
|
||||
const sessions = getLocalStorageItem(key, defaultSessions);
|
||||
|
||||
export type Sessions = Session[];
|
||||
export type SessionStoreName = {
|
||||
sync: string;
|
||||
crypto: string;
|
||||
};
|
||||
// Before multi account support session was stored
|
||||
// as multiple item in local storage.
|
||||
// So we need these migration code.
|
||||
const fallbackSession = getFallbackSession();
|
||||
if (fallbackSession) {
|
||||
sessions.push(fallbackSession);
|
||||
setLocalStorageItem(key, sessions);
|
||||
removeFallbackSession();
|
||||
}
|
||||
return sessions;
|
||||
},
|
||||
(key, value) => {
|
||||
setLocalStorageItem(key, value);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Migration code for old session
|
||||
*/
|
||||
// const FALLBACK_STORE_NAME: SessionStoreName = {
|
||||
// sync: 'web-sync-store',
|
||||
// crypto: 'crypto-store',
|
||||
// } as const;
|
||||
|
||||
export function setFallbackSession(
|
||||
accessToken: string,
|
||||
deviceId: string,
|
||||
userId: string,
|
||||
baseUrl: string
|
||||
) {
|
||||
localStorage.setItem('cinny_access_token', accessToken);
|
||||
localStorage.setItem('cinny_device_id', deviceId);
|
||||
localStorage.setItem('cinny_user_id', userId);
|
||||
localStorage.setItem('cinny_hs_base_url', baseUrl);
|
||||
}
|
||||
export const removeFallbackSession = () => {
|
||||
localStorage.removeItem('cinny_hs_base_url');
|
||||
localStorage.removeItem('cinny_user_id');
|
||||
localStorage.removeItem('cinny_device_id');
|
||||
localStorage.removeItem('cinny_access_token');
|
||||
};
|
||||
export const getFallbackSession = (): Session | undefined => {
|
||||
const baseUrl = localStorage.getItem('cinny_hs_base_url');
|
||||
const userId = localStorage.getItem('cinny_user_id');
|
||||
const deviceId = localStorage.getItem('cinny_device_id');
|
||||
const accessToken = localStorage.getItem('cinny_access_token');
|
||||
|
||||
if (baseUrl && userId && deviceId && accessToken) {
|
||||
const session: Session = {
|
||||
baseUrl,
|
||||
userId,
|
||||
deviceId,
|
||||
accessToken,
|
||||
fallbackSdkStores: true,
|
||||
export type SessionsAction =
|
||||
| {
|
||||
type: 'PUT';
|
||||
session: Session;
|
||||
}
|
||||
| {
|
||||
type: 'DELETE';
|
||||
session: Session;
|
||||
};
|
||||
|
||||
return session;
|
||||
export const sessionsAtom = atom<Sessions, [SessionsAction], undefined>(
|
||||
(get) => get(baseSessionsAtom),
|
||||
(get, set, action) => {
|
||||
if (action.type === 'PUT') {
|
||||
const sessions = [...get(baseSessionsAtom)];
|
||||
const sessionIndex = sessions.findIndex(
|
||||
(session) => session.userId === action.session.userId
|
||||
);
|
||||
if (sessionIndex === -1) {
|
||||
sessions.push(action.session);
|
||||
} else {
|
||||
sessions.splice(sessionIndex, 1, action.session);
|
||||
}
|
||||
set(baseSessionsAtom, sessions);
|
||||
return;
|
||||
}
|
||||
if (action.type === 'DELETE') {
|
||||
const sessions = get(baseSessionsAtom).filter(
|
||||
(session) => session.userId !== action.session.userId
|
||||
);
|
||||
set(baseSessionsAtom, sessions);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return undefined;
|
||||
};
|
||||
/**
|
||||
* End of migration code for old session
|
||||
*/
|
||||
|
||||
// export const getSessionStoreName = (session: Session): SessionStoreName => {
|
||||
// if (session.fallbackSdkStores) {
|
||||
// return FALLBACK_STORE_NAME;
|
||||
// }
|
||||
|
||||
// return {
|
||||
// sync: `sync${session.userId}`,
|
||||
// crypto: `crypto${session.userId}`,
|
||||
// };
|
||||
// };
|
||||
|
||||
// export const MATRIX_SESSIONS_KEY = 'matrixSessions';
|
||||
// const baseSessionsAtom = atomWithLocalStorage<Sessions>(
|
||||
// MATRIX_SESSIONS_KEY,
|
||||
// (key) => {
|
||||
// const defaultSessions: Sessions = [];
|
||||
// const sessions = getLocalStorageItem(key, defaultSessions);
|
||||
|
||||
// // Before multi account support session was stored
|
||||
// // as multiple item in local storage.
|
||||
// // So we need these migration code.
|
||||
// const fallbackSession = getFallbackSession();
|
||||
// if (fallbackSession) {
|
||||
// removeFallbackSession();
|
||||
// sessions.push(fallbackSession);
|
||||
// setLocalStorageItem(key, sessions);
|
||||
// }
|
||||
// return sessions;
|
||||
// },
|
||||
// (key, value) => {
|
||||
// setLocalStorageItem(key, value);
|
||||
// }
|
||||
// );
|
||||
|
||||
// export type SessionsAction =
|
||||
// | {
|
||||
// type: 'PUT';
|
||||
// session: Session;
|
||||
// }
|
||||
// | {
|
||||
// type: 'DELETE';
|
||||
// session: Session;
|
||||
// };
|
||||
|
||||
// export const sessionsAtom = atom<Sessions, [SessionsAction], undefined>(
|
||||
// (get) => get(baseSessionsAtom),
|
||||
// (get, set, action) => {
|
||||
// if (action.type === 'PUT') {
|
||||
// const sessions = [...get(baseSessionsAtom)];
|
||||
// const sessionIndex = sessions.findIndex(
|
||||
// (session) => session.userId === action.session.userId
|
||||
// );
|
||||
// if (sessionIndex === -1) {
|
||||
// sessions.push(action.session);
|
||||
// } else {
|
||||
// sessions.splice(sessionIndex, 1, action.session);
|
||||
// }
|
||||
// set(baseSessionsAtom, sessions);
|
||||
// return;
|
||||
// }
|
||||
// if (action.type === 'DELETE') {
|
||||
// const sessions = get(baseSessionsAtom).filter(
|
||||
// (session) => session.userId !== action.session.userId
|
||||
// );
|
||||
// set(baseSessionsAtom, sessions);
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
export const sessionUserIdAtom = atom<string | undefined>(undefined);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { atom } from 'jotai';
|
||||
import { WritableAtom } from 'jotai';
|
||||
import { atomWithLocalStorage } from './utils/atomWithLocalStorage';
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '../../client/localStorage';
|
||||
|
||||
const STORAGE_KEY = 'settings';
|
||||
export type DateFormat =
|
||||
| 'D MMM YYYY'
|
||||
| 'DD/MM/YYYY'
|
||||
|
|
@ -83,24 +84,31 @@ const defaultSettings: Settings = {
|
|||
developerTools: false,
|
||||
};
|
||||
|
||||
export const getSettings = () => {
|
||||
const settings = localStorage.getItem(STORAGE_KEY);
|
||||
if (settings === null) return defaultSettings;
|
||||
return {
|
||||
...defaultSettings,
|
||||
...(JSON.parse(settings) as Settings),
|
||||
};
|
||||
const STORAGE_KEY = 'settings';
|
||||
const getStoreKey = (userId: string): string => `${STORAGE_KEY}${userId}`;
|
||||
|
||||
// export const settingsAtom = atomWithLocalStorage<Settings>(
|
||||
// STORAGE_KEY,
|
||||
// (key) => getLocalStorageItem<Settings>(key, defaultSettings),
|
||||
// (key, settings) => {
|
||||
// setLocalStorageItem(key, settings);
|
||||
// }
|
||||
// );
|
||||
|
||||
export type SettingsAtom = WritableAtom<Settings, [Settings], void>;
|
||||
|
||||
export const makeSettingsAtom = (userId: string): SettingsAtom => {
|
||||
const settingsAtom = atomWithLocalStorage<Settings>(
|
||||
getStoreKey(userId),
|
||||
(key) => getLocalStorageItem<Settings>(key, defaultSettings),
|
||||
(key, settings) => {
|
||||
setLocalStorageItem(key, settings);
|
||||
}
|
||||
);
|
||||
|
||||
return settingsAtom;
|
||||
};
|
||||
|
||||
export const setSettings = (settings: Settings) => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
export const clearSettingsStore = (userId: string) => {
|
||||
localStorage.removeItem(getStoreKey(userId));
|
||||
};
|
||||
|
||||
const baseSettings = atom<Settings>(getSettings());
|
||||
export const settingsAtom = atom<Settings, [Settings], undefined>(
|
||||
(get) => get(baseSettings),
|
||||
(get, set, update) => {
|
||||
set(baseSettings, update);
|
||||
setSettings(update);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { atom } from 'jotai';
|
||||
import produce from 'immer';
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
setLocalStorageItem,
|
||||
} from './utils/atomWithLocalStorage';
|
||||
import { atomWithLocalStorage } from './utils/atomWithLocalStorage';
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '../../client/localStorage';
|
||||
|
||||
const SPACE_ROOMS = 'spaceRooms';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { atom, useSetAtom } from 'jotai';
|
|||
import { useEffect } from 'react';
|
||||
import { roomTypingEphemeral } from 'neurone/lib/utils/sync/v3/join';
|
||||
import { useSetting } from './hooks/settings';
|
||||
import { settingsAtom } from './settings';
|
||||
import { ClientContext } from '../../client/context';
|
||||
|
||||
export type IRoomIdToTypingMembers = Map<string, string[]>;
|
||||
|
|
@ -14,7 +13,7 @@ export const useBindRoomIdToTypingMembersAtom = (
|
|||
typingMembersAtom: typeof roomIdToTypingMembersAtom
|
||||
) => {
|
||||
const setTypingMembers = useSetAtom(typingMembersAtom);
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const [hideActivity] = useSetting('hideActivity');
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = ct.sync.observable.subscribe((syncValue) => {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,5 @@
|
|||
import { atom } from 'jotai';
|
||||
|
||||
export const getLocalStorageItem = <T>(key: string, defaultValue: T): T => {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item === null) return defaultValue;
|
||||
if (item === 'undefined') return undefined as T;
|
||||
try {
|
||||
return JSON.parse(item) as T;
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const setLocalStorageItem = <T>(key: string, value: T) => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
};
|
||||
|
||||
export type GetLocalStorageItem<T> = (key: string) => T;
|
||||
export type SetLocalStorageItem<T> = (key: string, value: T) => void;
|
||||
|
||||
|
|
@ -39,7 +24,7 @@ export const atomWithLocalStorage = <T>(
|
|||
};
|
||||
};
|
||||
|
||||
const localStorageAtom = atom<T, [T], undefined>(
|
||||
const localStorageAtom = atom<T, [T], void>(
|
||||
(get) => get(baseAtom),
|
||||
(get, set, newValue) => {
|
||||
set(baseAtom, newValue);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ export class LogoutManager implements Disposable {
|
|||
|
||||
private async clearStoresAndRefresh(): Promise<void> {
|
||||
await this.deleteStorage();
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
} from './storage/collections';
|
||||
import { Storage } from './storage';
|
||||
import { SyncContext, SyncValue } from './sync';
|
||||
import { Session } from '../app/state/sessions';
|
||||
import { Session } from './sessions';
|
||||
import { JoinRule, SpaceChildContent, StateEventType } from '../types/matrix/room';
|
||||
import { GetTxnId } from './txn';
|
||||
import { Timeline } from './room';
|
||||
|
|
|
|||
|
|
@ -2,13 +2,10 @@ import { deleteDB } from 'idb';
|
|||
import { Machine, OutgoingRequestSender } from 'neurone/lib/crypto/machine';
|
||||
import { MatrixRequest } from 'neurone/lib/matrix-request';
|
||||
import { MatrixRequestRetrier, sleep } from 'neurone/lib/utils';
|
||||
import { Session } from '../app/state/sessions';
|
||||
|
||||
const getCryptoStoreName = (userId: string): string => `crypto${userId}`;
|
||||
import { FALLBACK_SYNC_STORE_NAME_FULL, getSessionStoreName, Session } from './sessions';
|
||||
|
||||
// the matrix-crypto-sdk add postfix to storeName
|
||||
const getCryptoDBName = (userId: string): string =>
|
||||
`${getCryptoStoreName(userId)}::matrix-sdk-crypto`;
|
||||
const getCryptoDBName = (storeName: string): string => `${storeName}::matrix-sdk-crypto`;
|
||||
|
||||
let closeMachineRef: (() => void) | undefined;
|
||||
|
||||
|
|
@ -20,7 +17,7 @@ export const initializeCrypto = async (
|
|||
deleteCryptoStorage: () => Promise<void>;
|
||||
closeMachine: () => void;
|
||||
}> => {
|
||||
const storeName = getCryptoStoreName(session.userId);
|
||||
const storeName = getSessionStoreName(session).crypto;
|
||||
|
||||
const { olmMachine, closeMachine: closeMachineInner } = await Machine.init(
|
||||
session.userId,
|
||||
|
|
@ -43,13 +40,22 @@ export const initializeCrypto = async (
|
|||
|
||||
const deleteCryptoStorage = async () => {
|
||||
closeMachine();
|
||||
await deleteDB(getCryptoDBName(session.userId));
|
||||
await deleteDB(getCryptoDBName(storeName));
|
||||
};
|
||||
|
||||
return { machine, deleteCryptoStorage, closeMachine };
|
||||
};
|
||||
|
||||
export const deleteClientCryptoStorage = async (userId: string): Promise<void> => {
|
||||
export const deleteClientCryptoStorage = async (session: Session): Promise<void> => {
|
||||
closeMachineRef?.();
|
||||
await deleteDB(getCryptoDBName(userId));
|
||||
await deleteDB(getSessionStoreName(session).crypto);
|
||||
|
||||
if (session.fallbackSdkStores) {
|
||||
try {
|
||||
// migration code from matrix-js-sdk
|
||||
await deleteDB(FALLBACK_SYNC_STORE_NAME_FULL);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
|
||||
|
||||
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
||||
import { pushSessionToSW } from '../sw-session';
|
||||
|
||||
type Session = {
|
||||
baseUrl: string;
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
export const initClient = async (session: Session): Promise<MatrixClient> => {
|
||||
const indexedDBStore = new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
localStorage: global.localStorage,
|
||||
dbName: 'web-sync-store',
|
||||
});
|
||||
|
||||
const legacyCryptoStore = new IndexedDBCryptoStore(global.indexedDB, 'crypto-store');
|
||||
|
||||
const mx = createClient({
|
||||
baseUrl: session.baseUrl,
|
||||
accessToken: session.accessToken,
|
||||
userId: session.userId,
|
||||
store: indexedDBStore,
|
||||
cryptoStore: legacyCryptoStore,
|
||||
deviceId: session.deviceId,
|
||||
timelineSupport: true,
|
||||
verificationMethods: ['m.sas.v1'],
|
||||
});
|
||||
|
||||
await indexedDBStore.startup();
|
||||
await mx.initRustCrypto();
|
||||
|
||||
mx.setMaxListeners(50);
|
||||
|
||||
return mx;
|
||||
};
|
||||
|
||||
export const startClient = async (mx: MatrixClient) => {
|
||||
await mx.startClient({
|
||||
lazyLoadMembers: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const clearCacheAndReload = async (mx: MatrixClient) => {
|
||||
mx.stopClient();
|
||||
clearNavToActivePathStore(mx.getSafeUserId());
|
||||
await mx.store.deleteAllData();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
export const logoutClient = async (mx: MatrixClient) => {
|
||||
pushSessionToSW();
|
||||
mx.stopClient();
|
||||
try {
|
||||
await mx.logout();
|
||||
} catch {
|
||||
// ignore if failed to logout
|
||||
}
|
||||
await mx.clearStores();
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
export const clearLoginData = async () => {
|
||||
const dbs = await window.indexedDB.databases();
|
||||
|
||||
dbs.forEach((idbInfo) => {
|
||||
const { name } = idbInfo;
|
||||
if (name) {
|
||||
window.indexedDB.deleteDatabase(name);
|
||||
}
|
||||
});
|
||||
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { MatrixRequest } from 'neurone/lib/matrix-request';
|
||||
import { Disposer } from 'neurone/lib/utils/disposable';
|
||||
import { Session } from '../app/state/sessions';
|
||||
import { removeSession, Session } from './sessions';
|
||||
import {
|
||||
ClientContext,
|
||||
createSecretsObservable,
|
||||
|
|
@ -38,15 +38,23 @@ import { MachineManager } from './MachineManager';
|
|||
import { DecryptionManager } from './decryption';
|
||||
import { initializeCrypto } from './crypto';
|
||||
import { MegolmBackup } from './MegolmBackup';
|
||||
import { clearUserLocalStorage } from './localStorage';
|
||||
|
||||
export const initializeClient = async (session: Session): Promise<ClientContext> => {
|
||||
export type InitializeClientResult = {
|
||||
ct: ClientContext;
|
||||
dispose: () => void;
|
||||
};
|
||||
export const initializeClient = async (session: Session): Promise<InitializeClientResult> => {
|
||||
const disposer = new Disposer();
|
||||
|
||||
const { storage, deleteStorage } = await createStorage(session.userId);
|
||||
const { storage, deleteStorage, disconnectStorage } = await createStorage(session);
|
||||
|
||||
const requestAuthJSON: MatrixRequest = getAuthJSONRequest(session.baseUrl, session.accessToken);
|
||||
|
||||
const { machine, deleteCryptoStorage } = await initializeCrypto(session, requestAuthJSON);
|
||||
const { machine, deleteCryptoStorage, closeMachine } = await initializeCrypto(
|
||||
session,
|
||||
requestAuthJSON
|
||||
);
|
||||
await machine.outgoingRequests.processAll();
|
||||
|
||||
const state = await createStateContext(storage, disposer);
|
||||
|
|
@ -98,6 +106,8 @@ export const initializeClient = async (session: Session): Promise<ClientContext>
|
|||
disposer.run();
|
||||
await deleteCryptoStorage();
|
||||
await deleteStorage();
|
||||
clearUserLocalStorage(session.userId);
|
||||
removeSession(session);
|
||||
});
|
||||
|
||||
const clearCacheAndReload = async () => {
|
||||
|
|
@ -160,6 +170,8 @@ export const initializeClient = async (session: Session): Promise<ClientContext>
|
|||
});
|
||||
|
||||
disposer.collect(
|
||||
disconnectStorage,
|
||||
closeMachine,
|
||||
megolmBackup,
|
||||
decryption,
|
||||
syncObservable,
|
||||
|
|
@ -172,5 +184,8 @@ export const initializeClient = async (session: Session): Promise<ClientContext>
|
|||
syncResponseManager
|
||||
);
|
||||
|
||||
return ct;
|
||||
return {
|
||||
ct,
|
||||
dispose: () => disposer.run(),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
30
src/client/localStorage.ts
Normal file
30
src/client/localStorage.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { clearCallPreferenceStore } from '../app/state/callPreferences';
|
||||
import { clearClosedLobbyCategoriesStore } from '../app/state/closedLobbyCategories';
|
||||
import { clearClosedNavCategoriesStore } from '../app/state/closedNavCategories';
|
||||
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
||||
import { clearOpenedSidebarFolderStore } from '../app/state/openedSidebarFolder';
|
||||
import { clearSettingsStore } from '../app/state/settings';
|
||||
|
||||
export const getLocalStorageItem = <T>(key: string, defaultValue: T): T => {
|
||||
const item = window.localStorage.getItem(key);
|
||||
if (item === null) return defaultValue;
|
||||
if (item === 'undefined') return undefined as T;
|
||||
try {
|
||||
return JSON.parse(item) as T;
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const setLocalStorageItem = <T>(key: string, value: T) => {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
};
|
||||
|
||||
export const clearUserLocalStorage = (userId: string) => {
|
||||
clearClosedNavCategoriesStore(userId);
|
||||
clearClosedLobbyCategoriesStore(userId);
|
||||
clearNavToActivePathStore(userId);
|
||||
clearOpenedSidebarFolderStore(userId);
|
||||
clearCallPreferenceStore(userId);
|
||||
clearSettingsStore(userId);
|
||||
};
|
||||
97
src/client/sessions.ts
Normal file
97
src/client/sessions.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { getLocalStorageItem, setLocalStorageItem } from './localStorage';
|
||||
|
||||
export const CLIENT_SESSIONS_STORE_KEY = 'client-sessions';
|
||||
|
||||
export type Session = {
|
||||
baseUrl: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
accessToken: string;
|
||||
expiresInMs?: number;
|
||||
refreshToken?: string;
|
||||
fallbackSdkStores?: boolean;
|
||||
};
|
||||
|
||||
export type Sessions = Session[];
|
||||
export type SessionStoreName = {
|
||||
client: string;
|
||||
crypto: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration code for old session
|
||||
*/
|
||||
const FALLBACK_CRYPTO_STORE_NAME = 'matrix-js-sdk';
|
||||
export const FALLBACK_SYNC_STORE_NAME_FULL = 'matrix-js-sdk::web-sync-store';
|
||||
|
||||
export function setFallbackSession(
|
||||
accessToken: string,
|
||||
deviceId: string,
|
||||
userId: string,
|
||||
baseUrl: string
|
||||
) {
|
||||
localStorage.setItem('cinny_access_token', accessToken);
|
||||
localStorage.setItem('cinny_device_id', deviceId);
|
||||
localStorage.setItem('cinny_user_id', userId);
|
||||
localStorage.setItem('cinny_hs_base_url', baseUrl);
|
||||
}
|
||||
export const removeFallbackSession = () => {
|
||||
localStorage.removeItem('cinny_hs_base_url');
|
||||
localStorage.removeItem('cinny_user_id');
|
||||
localStorage.removeItem('cinny_device_id');
|
||||
localStorage.removeItem('cinny_access_token');
|
||||
};
|
||||
export const getFallbackSession = (): Session | undefined => {
|
||||
const baseUrl = localStorage.getItem('cinny_hs_base_url');
|
||||
const userId = localStorage.getItem('cinny_user_id');
|
||||
const deviceId = localStorage.getItem('cinny_device_id');
|
||||
const accessToken = localStorage.getItem('cinny_access_token');
|
||||
|
||||
if (baseUrl && userId && deviceId && accessToken) {
|
||||
const session: Session = {
|
||||
baseUrl,
|
||||
userId,
|
||||
deviceId,
|
||||
accessToken,
|
||||
fallbackSdkStores: true,
|
||||
};
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
/**
|
||||
* End of migration code for old session
|
||||
*/
|
||||
|
||||
export const getSessionStoreName = (session: Session): SessionStoreName => {
|
||||
if (session.fallbackSdkStores) {
|
||||
// if session is recovered from fallback
|
||||
// reuse the current crypto store.
|
||||
// old sdk uses the same sdk/idb as new one.
|
||||
return {
|
||||
client: `client${session.userId}`,
|
||||
crypto: FALLBACK_CRYPTO_STORE_NAME,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
client: `client${session.userId}`,
|
||||
crypto: `crypto${session.userId}`,
|
||||
};
|
||||
};
|
||||
|
||||
export const userHasSession = (userId: string): boolean => {
|
||||
const sessions = getLocalStorageItem<Sessions>(CLIENT_SESSIONS_STORE_KEY, []);
|
||||
|
||||
return sessions.some((s) => s.userId === userId);
|
||||
};
|
||||
|
||||
export const removeSession = (session: Session): void => {
|
||||
const sessions = getLocalStorageItem<Sessions>(CLIENT_SESSIONS_STORE_KEY, []);
|
||||
|
||||
const newSessions = sessions.filter((s) => s.userId === session.userId);
|
||||
|
||||
setLocalStorageItem(CLIENT_SESSIONS_STORE_KEY, newSessions);
|
||||
};
|
||||
|
|
@ -47,6 +47,7 @@ import {
|
|||
createJoinRoomLatestEventCollection,
|
||||
prepareJoinRoomLatestEventStore,
|
||||
} from './collections';
|
||||
import { getSessionStoreName, Session } from '../sessions';
|
||||
|
||||
export * from './utils';
|
||||
|
||||
|
|
@ -78,20 +79,18 @@ export interface Storage {
|
|||
presence: PresenceCollection;
|
||||
}
|
||||
|
||||
const getDBName = (userId: string): string => `client${userId}`;
|
||||
|
||||
// create a reference to opened database close callback.
|
||||
// used in clearing cache if client failed to initilize.
|
||||
let disconnectStorageRef: (() => void) | undefined;
|
||||
|
||||
export const createStorage = async (
|
||||
userId: string
|
||||
session: Session
|
||||
): Promise<{
|
||||
storage: Storage;
|
||||
deleteStorage: () => Promise<void>;
|
||||
disconnectStorage: () => void;
|
||||
}> => {
|
||||
const dbName = getDBName(userId);
|
||||
const dbName = getSessionStoreName(session).client;
|
||||
|
||||
// TODO: handle all openDB callbacks.
|
||||
const idb = await openDB(dbName, DATABASE_VERSION, {
|
||||
|
|
@ -165,8 +164,8 @@ export const createStorage = async (
|
|||
return { storage, deleteStorage, disconnectStorage };
|
||||
};
|
||||
|
||||
export const deleteClientStorage = async (userId: string) => {
|
||||
export const deleteClientStorage = async (session: Session) => {
|
||||
disconnectStorageRef?.();
|
||||
const dbName = getDBName(userId);
|
||||
const dbName = getSessionStoreName(session).client;
|
||||
await deleteDB(dbName);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,10 +15,13 @@ import App from './app/pages/App';
|
|||
|
||||
// import i18n (needs to be bundled ;))
|
||||
import './app/i18n';
|
||||
import { pushSessionToSW } from './sw-session';
|
||||
import { getFallbackSession } from './app/state/sessions';
|
||||
import { ContainerColor } from './app/styles/ContainerColor.css';
|
||||
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
document.documentElement.classList.add(
|
||||
configClass,
|
||||
varsClass,
|
||||
...ContainerColor({ variant: 'Background' }).split(' ')
|
||||
);
|
||||
|
||||
// Register Service Worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
|
@ -27,21 +30,7 @@ if ('serviceWorker' in navigator) {
|
|||
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
|
||||
: `/dev-sw.js?dev-sw`;
|
||||
|
||||
const sendSessionToSW = () => {
|
||||
const session = getFallbackSession();
|
||||
pushSessionToSW(session?.baseUrl, session?.accessToken);
|
||||
};
|
||||
|
||||
navigator.serviceWorker.register(swUrl).then(sendSessionToSW);
|
||||
navigator.serviceWorker.ready.then(sendSessionToSW);
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', (ev) => {
|
||||
const { type } = ev.data ?? {};
|
||||
|
||||
if (type === 'requestSession') {
|
||||
sendSessionToSW();
|
||||
}
|
||||
});
|
||||
navigator.serviceWorker.register(swUrl);
|
||||
}
|
||||
|
||||
const mountApp = () => {
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
export function pushSessionToSW(baseUrl?: string, accessToken?: string) {
|
||||
if (!('serviceWorker' in navigator)) return;
|
||||
if (!navigator.serviceWorker.controller) return;
|
||||
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'setSession',
|
||||
accessToken,
|
||||
baseUrl,
|
||||
});
|
||||
}
|
||||
93
src/sw.ts
93
src/sw.ts
|
|
@ -8,69 +8,30 @@ type SessionInfo = {
|
|||
baseUrl: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Store session per client (tab)
|
||||
*/
|
||||
const sessions = new Map<string, SessionInfo>();
|
||||
let _requestId = 0;
|
||||
const getRequestId = () => {
|
||||
_requestId += 1;
|
||||
return _requestId;
|
||||
};
|
||||
|
||||
const clientToResolve = new Map<string, (value: SessionInfo | undefined) => void>();
|
||||
const clientToSessionPromise = new Map<string, Promise<SessionInfo | undefined>>();
|
||||
const pendingSessionRequests = new Map<number, (sessionInfo: SessionInfo | undefined) => void>();
|
||||
|
||||
async function cleanupDeadClients() {
|
||||
const activeClients = await self.clients.matchAll();
|
||||
const activeIds = new Set(activeClients.map((c) => c.id));
|
||||
|
||||
Array.from(sessions.keys()).forEach((id) => {
|
||||
if (!activeIds.has(id)) {
|
||||
sessions.delete(id);
|
||||
clientToResolve.delete(id);
|
||||
clientToSessionPromise.delete(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setSession(clientId: string, accessToken: any, baseUrl: any) {
|
||||
if (typeof accessToken === 'string' && typeof baseUrl === 'string') {
|
||||
sessions.set(clientId, { accessToken, baseUrl });
|
||||
} else {
|
||||
// Logout or invalid session
|
||||
sessions.delete(clientId);
|
||||
}
|
||||
|
||||
const resolveSession = clientToResolve.get(clientId);
|
||||
if (resolveSession) {
|
||||
resolveSession(sessions.get(clientId));
|
||||
clientToResolve.delete(clientId);
|
||||
clientToSessionPromise.delete(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
function requestSession(client: Client): Promise<SessionInfo | undefined> {
|
||||
const promise =
|
||||
clientToSessionPromise.get(client.id) ??
|
||||
new Promise((resolve) => {
|
||||
clientToResolve.set(client.id, resolve);
|
||||
client.postMessage({ type: 'requestSession' });
|
||||
});
|
||||
|
||||
if (!clientToSessionPromise.has(client.id)) {
|
||||
clientToSessionPromise.set(client.id, promise);
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function requestSessionWithTimeout(
|
||||
clientId: string,
|
||||
timeoutMs = 3000
|
||||
): Promise<SessionInfo | undefined> {
|
||||
async function requestSession(clientId: string): Promise<SessionInfo | undefined> {
|
||||
const client = await self.clients.get(clientId);
|
||||
if (!client) return undefined;
|
||||
|
||||
const sessionPromise = requestSession(client);
|
||||
const id = getRequestId();
|
||||
|
||||
const sessionPromise = new Promise<SessionInfo | undefined>((resolve) => {
|
||||
pendingSessionRequests.set(id, resolve);
|
||||
client.postMessage({ type: 'session-request', id });
|
||||
});
|
||||
|
||||
const timeout = new Promise<undefined>((resolve) => {
|
||||
setTimeout(() => resolve(undefined), timeoutMs);
|
||||
setTimeout(() => {
|
||||
resolve(undefined);
|
||||
pendingSessionRequests.delete(id);
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
return Promise.race([sessionPromise, timeout]);
|
||||
|
|
@ -84,7 +45,6 @@ self.addEventListener('activate', (event: ExtendableEvent) => {
|
|||
event.waitUntil(
|
||||
(async () => {
|
||||
await self.clients.claim();
|
||||
await cleanupDeadClients();
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
|
@ -96,11 +56,12 @@ self.addEventListener('message', (event: ExtendableMessageEvent) => {
|
|||
const client = event.source as Client | null;
|
||||
if (!client) return;
|
||||
|
||||
const { type, accessToken, baseUrl } = event.data || {};
|
||||
const { type, id, accessToken, baseUrl } = event.data || {};
|
||||
|
||||
if (type === 'setSession') {
|
||||
setSession(client.id, accessToken, baseUrl);
|
||||
cleanupDeadClients();
|
||||
if (type === 'session-request') {
|
||||
const requestResolver = pendingSessionRequests.get(id);
|
||||
pendingSessionRequests.delete(id);
|
||||
requestResolver?.({ accessToken, baseUrl });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -139,16 +100,8 @@ self.addEventListener('fetch', (event: FetchEvent) => {
|
|||
const { clientId } = event;
|
||||
if (!clientId) return;
|
||||
|
||||
const session = sessions.get(clientId);
|
||||
if (session) {
|
||||
if (validMediaRequest(url, session.baseUrl)) {
|
||||
event.respondWith(fetch(url, fetchConfig(session.accessToken)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
requestSessionWithTimeout(clientId).then((s) => {
|
||||
requestSession(clientId).then((s) => {
|
||||
if (s && validMediaRequest(url, s.baseUrl)) {
|
||||
return fetch(url, fetchConfig(s.accessToken));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue