diff --git a/src/app/pages/auth/OrDivider.tsx b/src/app/components/OrDivider.tsx
similarity index 100%
rename from src/app/pages/auth/OrDivider.tsx
rename to src/app/components/OrDivider.tsx
diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/components/auth-layout/AuthFooter.tsx
similarity index 100%
rename from src/app/pages/auth/AuthFooter.tsx
rename to src/app/components/auth-layout/AuthFooter.tsx
diff --git a/src/app/components/auth-layout/AuthLayout.tsx b/src/app/components/auth-layout/AuthLayout.tsx
new file mode 100644
index 00000000..a57e7963
--- /dev/null
+++ b/src/app/components/auth-layout/AuthLayout.tsx
@@ -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
;
+}
+
+type AuthLayoutProps = {
+ hideHeader?: boolean;
+ center?: boolean;
+ children?: ReactNode;
+};
+export function AuthLayout({ hideHeader, center, children }: AuthLayoutProps) {
+ return (
+
+
+
+
+ {!hideHeader && (
+
+ )}
+
+ {children}
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/components/auth-layout/index.ts b/src/app/components/auth-layout/index.ts
new file mode 100644
index 00000000..654e0e04
--- /dev/null
+++ b/src/app/components/auth-layout/index.ts
@@ -0,0 +1 @@
+export * from './AuthLayout';
diff --git a/src/app/components/auth-layout/styles.css.ts b/src/app/components/auth-layout/styles.css.ts
new file mode 100644
index 00000000..649c4beb
--- /dev/null
+++ b/src/app/components/auth-layout/styles.css.ts
@@ -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,
+});
diff --git a/src/app/components/editor/Toolbar.tsx b/src/app/components/editor/Toolbar.tsx
index 7d701c42..54fc8f85 100644
--- a/src/app/components/editor/Toolbar.tsx
+++ b/src/app/components/editor/Toolbar.tsx
@@ -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 (
diff --git a/src/app/components/room-intro/RoomIntro.tsx b/src/app/components/room-intro/RoomIntro.tsx
index ff17c964..88ce2b33 100644
--- a/src/app/components/room-intro/RoomIntro.tsx
+++ b/src/app/components/room-intro/RoomIntro.tsx
@@ -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 (
diff --git a/src/app/components/time-date/TimePicker.tsx b/src/app/components/time-date/TimePicker.tsx
index c835ed09..03731b47 100644
--- a/src/app/components/time-date/TimePicker.tsx
+++ b/src/app/components/time-date/TimePicker.tsx
@@ -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(
({ min, max, value, onChange }, ref) => {
- const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [hour24Clock] = useSetting('hour24Clock');
const hour24 = dayjs(value).hour();
diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx
index 2273aaab..86098a6a 100644
--- a/src/app/components/user-profile/UserHero.tsx
+++ b/src/app/components/user-profile/UserHero.tsx
@@ -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;
diff --git a/src/app/components/user-profile/UserModeration.tsx b/src/app/components/user-profile/UserModeration.tsx
index 0db24f4b..6bdb2774 100644
--- a/src/app/components/user-profile/UserModeration.tsx
+++ b/src/app/components/user-profile/UserModeration.tsx
@@ -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;
diff --git a/src/app/features/common-settings/developer-tools/DevelopTools.tsx b/src/app/features/common-settings/developer-tools/DevelopTools.tsx
index 7d12a7c9..0ce6f377 100644
--- a/src/app/features/common-settings/developer-tools/DevelopTools.tsx
+++ b/src/app/features/common-settings/developer-tools/DevelopTools.tsx
@@ -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 (
diff --git a/src/app/features/common-settings/members/Members.tsx b/src/app/features/common-settings/members/Members.tsx
index 1e185fcf..85597842 100644
--- a/src/app/features/common-settings/members/Members.tsx
+++ b/src/app/features/common-settings/members/Members.tsx
@@ -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);
diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx
index 6d9ae85c..e5baa199 100644
--- a/src/app/features/lobby/Lobby.tsx
+++ b/src/app/features/lobby/Lobby.tsx
@@ -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(null);
const [heroSectionHeight, setHeroSectionHeight] = useState();
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());
diff --git a/src/app/features/lobby/LobbyHeader.tsx b/src/app/features/lobby/LobbyHeader.tsx
index f9692525..9e1b514c 100644
--- a/src/app/features/lobby/LobbyHeader.tsx
+++ b/src/app/features/lobby/LobbyHeader.tsx
@@ -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();
const screenSize = useScreenSizeContext();
diff --git a/src/app/features/message-search/MessageSearch.tsx b/src/app/features/message-search/MessageSearch.tsx
index 26085b5f..3bcf1eed 100644
--- a/src/app/features/message-search/MessageSearch.tsx
+++ b/src/app/features/message-search/MessageSearch.tsx
@@ -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(null);
const scrollTopAnchorRef = useRef(null);
diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx
index 71d0dc77..48001ffc 100644
--- a/src/app/features/room-nav/RoomNavItem.tsx
+++ b/src/app/features/room-nav/RoomNavItem.tsx
@@ -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(
({ 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);
diff --git a/src/app/features/room/MembersDrawer.tsx b/src/app/features/room/MembersDrawer.tsx
index 968c4e0e..8d57f46a 100644
--- a/src/app/features/room/MembersDrawer.tsx
+++ b/src/app/features/room/MembersDrawer.tsx
@@ -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 (
@@ -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);
diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx
index 85301f94..bd6da8df 100644
--- a/src/app/features/room/Room.tsx
+++ b/src/app/features/room/Room.tsx
@@ -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(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);
diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx
index 9e3f3f4b..fc5a03f0 100644
--- a/src/app/features/room/RoomInput.tsx
+++ b/src/app/features/room/RoomInput.tsx
@@ -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(
({ 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(
const imagePackRooms: string[] = useImagePackRooms(roomId);
- const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
+ const [toolbar, setToolbar] = useSetting('editorToolbar');
const [autocompleteQuery, setAutocompleteQuery] =
useState>();
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index eae129b6..e99ce402 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -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}
diff --git a/src/app/features/room/RoomView.tsx b/src/app/features/room/RoomView.tsx
index d7b25737..6507fc58 100644
--- a/src/app/features/room/RoomView.tsx
+++ b/src/app/features/room/RoomView.tsx
@@ -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(null);
const roomViewRef = useRef(null);
- const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const [hideActivity] = useSetting('hideActivity');
const roomId = useRoom();
const editor = useEditor();
diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx
index 29d5e351..69d65dca 100644
--- a/src/app/features/room/RoomViewHeader.tsx
+++ b/src/app/features/room/RoomViewHeader.tsx
@@ -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(({ 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 = {
diff --git a/src/app/features/room/hooks/useSendMessage.ts b/src/app/features/room/hooks/useSendMessage.ts
index 1c3bcea3..48fc1f98 100644
--- a/src/app/features/room/hooks/useSendMessage.ts
+++ b/src/app/features/room/hooks/useSendMessage.ts
@@ -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();
diff --git a/src/app/features/room/jump-to-time/JumpToTime.tsx b/src/app/features/room/jump-to-time/JumpToTime.tsx
index 6b8c15bf..5c72877e 100644
--- a/src/app/features/room/jump-to-time/JumpToTime.tsx
+++ b/src/app/features/room/jump-to-time/JumpToTime.tsx
@@ -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();
const [datePickerCords, setDatePickerCords] = useState();
diff --git a/src/app/features/room/message/MessageEditor.tsx b/src/app/features/room/message/MessageEditor.tsx
index 339931c6..44f64f22 100644
--- a/src/app/features/room/message/MessageEditor.tsx
+++ b/src/app/features/room/message/MessageEditor.tsx
@@ -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();
diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx
index 0939f0d5..314dddcc 100644
--- a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx
+++ b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx
@@ -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(({ 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(null);
diff --git a/src/app/features/room/thread-view/ThreadView.tsx b/src/app/features/room/thread-view/ThreadView.tsx
index 69c41860..88fad377 100644
--- a/src/app/features/room/thread-view/ThreadView.tsx
+++ b/src/app/features/room/thread-view/ThreadView.tsx
@@ -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(null);
const threadViewRef = useRef(null);
- const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const [hideActivity] = useSetting('hideActivity');
useThreadFocus(threadTimeline.threadId, threadFocusRef, threadViewRef);
useEditorAutoFocus(editor, threadFocusRef, true);
diff --git a/src/app/features/room/thread-view/TimelineView.tsx b/src/app/features/room/thread-view/TimelineView.tsx
index 45db35bd..e2c68bbf 100644
--- a/src/app/features/room/thread-view/TimelineView.tsx
+++ b/src/app/features/room/thread-view/TimelineView.tsx
@@ -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}
diff --git a/src/app/features/settings/Settings.tsx b/src/app/features/settings/Settings.tsx
index a4d879e8..ea50a9a7 100644
--- a/src/app/features/settings/Settings.tsx
+++ b/src/app/features/settings/Settings.tsx
@@ -93,12 +93,82 @@ const useSettingsMenuItems = (): SettingsMenuItem[] =>
[]
);
+type NavigationListProps = {
+ activePage?: SettingsPages;
+ setActivePage: (page: SettingsPages) => void;
+};
+function NavigationList({ activePage, setActivePage }: NavigationListProps) {
+ const menuItems = useSettingsMenuItems();
+
+ return (
+
+ {menuItems.map((item) => (
+ }
+ onClick={() => setActivePage(item.page)}
+ >
+
+ {item.name}
+
+
+ ))}
+
+ );
+}
+
+function LogoutButton() {
+ return (
+
+ {(logout, setLogout) => (
+ <>
+ }
+ onClick={() => setLogout(true)}
+ >
+ Logout
+
+ {logout && (
+ }>
+
+ setLogout(false),
+ clickOutsideDeactivates: true,
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ setLogout(false)} />
+
+
+
+ )}
+ >
+ )}
+
+ );
+}
+
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 (
@@ -154,61 +225,10 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
-
- {menuItems.map((item) => (
- }
- onClick={() => setActivePage(item.page)}
- >
-
- {item.name}
-
-
- ))}
-
+
-
-
- {(logout, setLogout) => (
- <>
- }
- onClick={() => setLogout(true)}
- >
- Logout
-
- {logout && (
- }>
-
- setLogout(false),
- clickOutsideDeactivates: true,
- escapeDeactivates: stopPropagation,
- }}
- >
- setLogout(false)} />
-
-
-
- )}
- >
- )}
-
+
+
diff --git a/src/app/features/settings/developer-tools/DevelopTools.tsx b/src/app/features/settings/developer-tools/DevelopTools.tsx
index 8b7463df..72ef8a1b 100644
--- a/src/app/features/settings/developer-tools/DevelopTools.tsx
+++ b/src/app/features/settings/developer-tools/DevelopTools.tsx
@@ -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();
diff --git a/src/app/features/settings/devices/DeviceTile.tsx b/src/app/features/settings/devices/DeviceTile.tsx
index 6ddd1165..3b2aad11 100644
--- a/src/app/features/settings/devices/DeviceTile.tsx
+++ b/src/app/features/settings/devices/DeviceTile.tsx
@@ -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 (
diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx
index b861a060..7b685aab 100644
--- a/src/app/features/settings/general/General.tsx
+++ b/src/app/features/settings/general/General.tsx
@@ -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();
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 = (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 (
@@ -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 (
@@ -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 (
@@ -743,7 +743,7 @@ function Editor() {
function SelectMessageLayout() {
const [menuCords, setMenuCords] = useState();
- const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout');
+ const [messageLayout, setMessageLayout] = useSetting('messageLayout');
const messageLayoutItems = useMessageLayoutItems();
const handleMenu: MouseEventHandler = (evt) => {
@@ -812,7 +812,7 @@ function SelectMessageLayout() {
function SelectMessageSpacing() {
const [menuCords, setMenuCords] = useState();
- const [messageSpacing, setMessageSpacing] = useSetting(settingsAtom, 'messageSpacing');
+ const [messageSpacing, setMessageSpacing] = useSetting('messageSpacing');
const messageSpacingItems = useMessageSpacingItems();
const handleMenu: MouseEventHandler = (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 (
diff --git a/src/app/features/settings/notifications/SystemNotification.tsx b/src/app/features/settings/notifications/SystemNotification.tsx
index 11c077ff..ce2b0e22 100644
--- a/src/app/features/settings/notifications/SystemNotification.tsx
+++ b/src/app/features/settings/notifications/SystemNotification.tsx
@@ -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();
diff --git a/src/app/hooks/sw/useSessionRequest.ts b/src/app/hooks/sw/useSessionRequest.ts
new file mode 100644
index 00000000..cf86eea6
--- /dev/null
+++ b/src/app/hooks/sw/useSessionRequest.ts
@@ -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) => {
+ 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]);
+};
diff --git a/src/app/hooks/useRoomLatestRenderedEvent.ts b/src/app/hooks/useRoomLatestRenderedEvent.ts
deleted file mode 100644
index fd0ed9e5..00000000
--- a/src/app/hooks/useRoomLatestRenderedEvent.ts
+++ /dev/null
@@ -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();
-
- 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;
-};
diff --git a/src/app/hooks/useRoomNavigate.ts b/src/app/hooks/useRoomNavigate.ts
index 67912b4c..dd502430 100644
--- a/src/app/hooks/useRoomNavigate.ts
+++ b/src/app/hooks/useRoomNavigate.ts
@@ -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) => {
diff --git a/src/app/hooks/useSessions.ts b/src/app/hooks/useSessions.ts
new file mode 100644
index 00000000..0f371b99
--- /dev/null
+++ b/src/app/hooks/useSessions.ts
@@ -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(null);
+export const SessionContextProvider = SessionContext.Provider;
+
+export const useSession = () => {
+ const session = useContext(SessionContext);
+
+ if (!session) {
+ throw new Error('Session is not provided!');
+ }
+
+ return session;
+};
diff --git a/src/app/hooks/useTheme.ts b/src/app/hooks/useTheme.ts
index cdbb9dba..4bbc8530 100644
--- a/src/app/hooks/useTheme.ts
+++ b/src/app/hooks/useTheme.ts
@@ -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;
diff --git a/src/app/hooks/useUserProfile.ts b/src/app/hooks/useUserProfile.ts
index a24d12e8..f9900e81 100644
--- a/src/app/hooks/useUserProfile.ts
+++ b/src/app/hooks/useUserProfile.ts
@@ -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 => {
- 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);
+};
diff --git a/src/app/pages/AccountSelector.tsx b/src/app/pages/AccountSelector.tsx
new file mode 100644
index 00000000..9b012cce
--- /dev/null
+++ b/src/app/pages/AccountSelector.tsx
@@ -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 (
+
+
+ }
+ />
+
+ }
+ >
+
+
+
+ {displayName}
+
+
+
+
+ {username}
+
+
+ {server}
+
+
+
+
+
+ );
+}
+
+export function AccountSelector() {
+ useUnauthThemeManager();
+ const navigate = useNavigate();
+
+ const sessions = useSessions();
+ useSessionRequest(sessions[0]);
+
+ if (sessions.length === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ Welcome to Cinny
+
+
+ Choose an account to continue.
+
+
+
+
+
+
+ Accounts
+
+ {sessions.map((session) => (
+
+ ))}
+
+
+
+ }
+ onClick={() => navigate(getLoginPath())}
+ >
+
+ Add Account
+
+
+
+
+
+ Do not have an account? Register
+
+
+
+ );
+}
+
+export function ClientAccountSelector() {
+ const clientConfig = useClientConfig();
+
+ const prevUserIdRef = useRef();
+
+ 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 ;
+ }
+
+ if (sessions.length === 0) {
+ const afterLoginPath = getAppPathFromHref(
+ getOriginBaseUrl(clientConfig.hashRouter),
+ window.location.href
+ );
+ if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath);
+
+ return ;
+ }
+
+ return (
+
+
+ {/* reset url from previous account */}
+ {prevUserId && prevUserId !== userId && }
+
+ );
+}
diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx
index b21db50f..72580f00 100644
--- a/src/app/pages/Router.tsx
+++ b/src/app/pages/Router.tsx
@@ -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(
- {
- if (getFallbackSession()) return redirect(getHomePath());
- const afterLoginPath = getAppPathFromHref(getOriginBaseUrl(), window.location.href);
- if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath);
- return redirect(getLoginPath());
- }}
- />
- {
- if (getFallbackSession()) {
- return redirect(getHomePath());
- }
-
- return null;
- }}
- element={
- <>
-
-
- >
- }
- >
+ } />
+ }>
} />
} />
} />
- {
- const session = getFallbackSession();
- if (!session) {
- const afterLoginPath = getAppPathFromHref(
- getOriginBaseUrl(hashRouter),
- window.location.href
- );
- if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath);
- return redirect(getLoginPath());
- }
- return null;
- }}
- element={
-
-
-
-
-
-
-
-
-
- }
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- >
+ }>
-
-
- }
- >
-
-
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
}
>
- {mobile ? null : } />}
- } />
- join
} />
- {/* } /> */}
-
-
- }
- />
-
-
-
-
- }
- >
-
-
- }
- >
- {mobile ? null : } />}
- } />
-
-
-
- }
- />
-
-
-
+
+
}
>
-
- }
- >
- {mobile ? null : (
- {
- const { spaceIdOrAlias } = params;
- if (spaceIdOrAlias) {
- return redirect(getSpaceLobbyPath(spaceIdOrAlias));
- }
- return null;
- }}
- element={}
- />
- )}
- } />
- {/*} /> */}
-
-
-
}
- />
-
-
-
-
- }
- >
-
-
- }
- >
- {mobile ? null : (
+ >
+ {mobile ? null : } />}
+ } />
+ join} />
+ {/* } /> */}
redirect(getExploreFeaturedPath())}
- element={}
- />
- )}
- } />
- } />
-
- } />
-
-
-
+ path={_ROOM_PATH}
+ element={
+
+
+
}
- >
-
-
- }
- >
- {mobile ? null : (
- redirect(getInboxNotificationsPath())}
- element={}
/>
- )}
- } />
- } />
+
+
+
+
+ }
+ >
+
+
+ }
+ >
+ {mobile ? null : } />}
+ } />
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ >
+
+
+
+ }
+ >
+ {mobile ? null : (
+ {
+ const { spaceIdOrAlias } = params;
+ if (spaceIdOrAlias) {
+ return redirect(getSpaceLobbyPath(spaceIdOrAlias));
+ }
+ return null;
+ }}
+ element={}
+ />
+ )}
+ } />
+ {/*} /> */}
+
+
+
+ }
+ />
+
+
+
+
+ }
+ >
+
+
+ }
+ >
+ {mobile ? null : (
+ redirect(getExploreFeaturedPath())}
+ element={}
+ />
+ )}
+ } />
+ } />
+
+ } />
+
+
+
+ }
+ >
+
+
+ }
+ >
+ {mobile ? null : (
+ redirect(getInboxNotificationsPath())}
+ element={}
+ />
+ )}
+ } />
+ } />
+
Page not found} />
diff --git a/src/app/pages/ThemeManager.tsx b/src/app/pages/ThemeManager.tsx
index 69d50cdb..2f99199e 100644
--- a/src/app/pages/ThemeManager.tsx
+++ b/src/app/pages/ThemeManager.tsx
@@ -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]);
diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx
deleted file mode 100644
index 1b37360d..00000000
--- a/src/app/pages/auth/AuthLayout.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {message}
-
-
- );
-}
-
-function AuthLayoutError({ message }: { message: string }) {
- return (
-
-
- {message}
-
-
- );
-}
-
-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 (
-
-
-
-
-
-
- Cinny
-
-
-
-
-
- Homeserver
-
-
-
- {discoveryState.status === AsyncStatus.Loading && (
-
- )}
- {discoveryState.status === AsyncStatus.Error && (
-
- )}
- {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && (
-
- )}
- {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && (
-
- )}
- {discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && (
-
-
- (
-
- )}
- error={() => (
-
- )}
- >
- {(specVersions) => (
-
- (
-
- )}
- error={() => (
-
- )}
- >
- {(authFlows) => (
-
-
-
- )}
-
-
- )}
-
-
-
- )}
-
-
-
-
-
- );
-}
diff --git a/src/app/pages/auth/AuthRoot.tsx b/src/app/pages/auth/AuthRoot.tsx
new file mode 100644
index 00000000..afec36e4
--- /dev/null
+++ b/src/app/pages/auth/AuthRoot.tsx
@@ -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 (
+
+
+
+ {message}
+
+
+ );
+}
+
+function AuthLayoutError({ message }: { message: string }) {
+ return (
+
+
+ {message}
+
+
+ );
+}
+
+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 (
+
+
+
+ Homeserver
+
+
+
+ {discoveryState.status === AsyncStatus.Loading && (
+
+ )}
+ {discoveryState.status === AsyncStatus.Error && (
+
+ )}
+ {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && (
+
+ )}
+ {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && (
+
+ )}
+ {discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && (
+
+
+ (
+
+ )}
+ error={() => (
+
+ )}
+ >
+ {(specVersions) => (
+
+ }
+ error={() => (
+
+ )}
+ >
+ {(authFlows) => (
+
+
+
+ )}
+
+
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/pages/auth/index.ts b/src/app/pages/auth/index.ts
index c4bd0476..50a6ce8c 100644
--- a/src/app/pages/auth/index.ts
+++ b/src/app/pages/auth/index.ts
@@ -1,4 +1,4 @@
-export * from './AuthLayout';
+export * from './AuthRoot';
export * from './login';
export * from './register';
export * from './reset-password';
diff --git a/src/app/pages/auth/login/Login.tsx b/src/app/pages/auth/login/Login.tsx
index e6fad6a0..0dab2c16 100644
--- a/src/app/pages/auth/login/Login.tsx
+++ b/src/app/pages/auth/login/Login.tsx
@@ -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() {
>
)}
-
+
Do not have an account? Register
diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx
index 89204a89..1777bc2a 100644
--- a/src/app/pages/auth/login/PasswordLoginForm.tsx
+++ b/src/app/pages/auth/login/PasswordLoginForm.tsx
@@ -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();
@@ -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);
});
};
diff --git a/src/app/pages/auth/login/TokenLogin.tsx b/src/app/pages/auth/login/TokenLogin.tsx
index 33f73e62..49eeb2ca 100644
--- a/src/app/pages/auth/login/TokenLogin.tsx
+++ b/src/app/pages/auth/login/TokenLogin.tsx
@@ -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 (
<>
diff --git a/src/app/pages/auth/login/loginUtil.ts b/src/app/pages/auth/login/loginUtil.ts
index 0922572a..4c2c77a7 100644
--- a/src/app/pages/auth/login/loginUtil.ts
+++ b/src/app/pages/auth/login/loginUtil.ts
@@ -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;
};
diff --git a/src/app/pages/auth/register/PasswordRegisterForm.tsx b/src/app/pages/auth/register/PasswordRegisterForm.tsx
index 30667963..aef0b1d3 100644
--- a/src/app/pages/auth/register/PasswordRegisterForm.tsx
+++ b/src/app/pages/auth/register/PasswordRegisterForm.tsx
@@ -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 = (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);
});
};
diff --git a/src/app/pages/auth/register/Register.tsx b/src/app/pages/auth/register/Register.tsx
index f937dca0..29fc3af2 100644
--- a/src/app/pages/auth/register/Register.tsx
+++ b/src/app/pages/auth/register/Register.tsx
@@ -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() {
>
)}
-
+
Already have an account? Login
diff --git a/src/app/pages/auth/register/registerUtil.ts b/src/app/pages/auth/register/registerUtil.ts
index 24e7a8c0..9c276953 100644
--- a/src/app/pages/auth/register/registerUtil.ts
+++ b/src/app/pages/auth/register/registerUtil.ts
@@ -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;
};
diff --git a/src/app/pages/client/ClientInitStorageAtom.tsx b/src/app/pages/client/ClientInitStorageAtom.tsx
index de5438cf..15b83840 100644
--- a/src/app/pages/client/ClientInitStorageAtom.tsx
+++ b/src/app/pages/client/ClientInitStorageAtom.tsx
@@ -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 (
- {children}
+ {children}
diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx
index 0df6f9f0..0c21785a 100644
--- a/src/app/pages/client/ClientNonUIFeatures.tsx
+++ b/src/app/pages/client/ClientNonUIFeatures.tsx
@@ -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');
diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx
index a63a98fb..4cb1091f 100644
--- a/src/app/pages/client/ClientRoot.tsx
+++ b/src/app/pages/client/ClientRoot.tsx
@@ -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(
- useCallback(() => {
- const session = getFallbackSession();
- if (!session) {
- throw new Error('No session Found!');
- }
- return initializeClient(session);
- }, [])
+ const [initState, initialize] = useAsyncCallback(
+ 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 (
-
- {initState.status !== AsyncStatus.Success && }
-
+
+ {initState.status !== AsyncStatus.Success && }
+
{initState.status === AsyncStatus.Error && (
)}
{initState.status === AsyncStatus.Success && (
-
+
diff --git a/src/app/pages/client/ClientRootOptions.tsx b/src/app/pages/client/ClientRootOptions.tsx
index eecf7d34..20b3f7d4 100644
--- a/src/app/pages/client/ClientRootOptions.tsx
+++ b/src/app/pages/client/ClientRootOptions.tsx
@@ -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();
const handleToggle: MouseEventHandler = (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 (
diff --git a/src/app/pages/client/InitialSyncSplashScreen.tsx b/src/app/pages/client/InitialSyncSplashScreen.tsx
index 6bfc8c81..5884fd2d 100644
--- a/src/app/pages/client/InitialSyncSplashScreen.tsx
+++ b/src/app/pages/client/InitialSyncSplashScreen.tsx
@@ -121,7 +121,7 @@ export function InitialSyncSplashScreen({ children }: { children: ReactNode }) {
if (initialSync) {
return (
<>
-
+
;
>
);
diff --git a/src/app/pages/client/direct/Direct.tsx b/src/app/pages/client/direct/Direct.tsx
index c9a27cfa..9463692d 100644
--- a/src/app/pages/client/direct/Direct.tsx
+++ b/src/app/pages/client/direct/Direct.tsx
@@ -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(({ requestClose }, ref) => {
- const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const [hideActivity] = useSetting('hideActivity');
const directRooms = useDirectRooms();
const roomsNotifications = useRoomsNotifications(directRooms, roomToNotificationsAtom);
const notifications = useRoomsTotalNotifications(roomsNotifications);
diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx
index 2a849eb8..cc1b8fe0 100644
--- a/src/app/pages/client/home/Home.tsx
+++ b/src/app/pages/client/home/Home.tsx
@@ -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(({ requestClose }, ref) => {
const orphanRooms = useHomeRooms();
- const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const [hideActivity] = useSetting('hideActivity');
const roomsNotifications = useRoomsNotifications(orphanRooms, roomToNotificationsAtom);
const notifications = useRoomsTotalNotifications(roomsNotifications);
diff --git a/src/app/pages/client/inbox/Invites.tsx b/src/app/pages/client/inbox/Invites.tsx
index 1c92bb56..35a5c27e 100644
--- a/src/app/pages/client/inbox/Invites.tsx
+++ b/src/app/pages/client/inbox/Invites.tsx
@@ -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) {
diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx
index 77ebe3d1..c870bb89 100644
--- a/src/app/pages/client/inbox/Notifications.tsx
+++ b/src/app/pages/client/inbox/Notifications.tsx
@@ -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);
diff --git a/src/app/pages/client/sidebar/DirectTab.tsx b/src/app/pages/client/sidebar/DirectTab.tsx
index b5249917..2c2be29e 100644
--- a/src/app/pages/client/sidebar/DirectTab.tsx
+++ b/src/app/pages/client/sidebar/DirectTab.tsx
@@ -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(
({ directRooms, roomToNotifications, notifications, requestClose }, ref) => {
- const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const [hideActivity] = useSetting('hideActivity');
const handleMarkAsRead = () => {
directRooms.forEach((rId) => {
diff --git a/src/app/pages/client/sidebar/HomeTab.tsx b/src/app/pages/client/sidebar/HomeTab.tsx
index 2ba53ceb..b694af6a 100644
--- a/src/app/pages/client/sidebar/HomeTab.tsx
+++ b/src/app/pages/client/sidebar/HomeTab.tsx
@@ -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(
({ homeRooms, roomToNotifications, notifications, requestClose }, ref) => {
- const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const [hideActivity] = useSetting('hideActivity');
const handleMarkAsRead = () => {
homeRooms.forEach((rId) => {
diff --git a/src/app/pages/client/sidebar/SettingsTab.tsx b/src/app/pages/client/sidebar/SettingsTab.tsx
index f9c0ef6e..a21a9039 100644
--- a/src/app/pages/client/sidebar/SettingsTab.tsx
+++ b/src/app/pages/client/sidebar/SettingsTab.tsx
@@ -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 (
+
+ );
+}
+
+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 (
+ requestClose(),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
+ isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+
+
+ }
+ />
+ );
+}
export function SettingsTab() {
const ct = useClient();
@@ -28,14 +176,43 @@ export function SettingsTab() {
})
: undefined;
- const openSettings = () => setSettings(true);
+ const [menuAnchor, setMenuAnchor] = useState();
+
+ const openSettings = () => {
+ setMenuAnchor(undefined);
+ setSettings(true);
+ };
const closeSettings = () => setSettings(false);
+ const handleOpenMenu: MouseEventHandler = (evt) => {
+ const cords = evt.currentTarget.getBoundingClientRect();
+ setMenuAnchor((currentState) => {
+ if (currentState) return undefined;
+ return cords;
+ });
+ };
+
+ const [switcherAnchor, setSwitcherAnchor] = useState();
+
+ const handleOpenAccountSwitcher: MouseEventHandler = (evt) => {
+ evt.preventDefault();
+ const cords = evt.currentTarget.getBoundingClientRect();
+ setSwitcherAnchor((currentState) => {
+ if (currentState) return undefined;
+ return cords;
+ });
+ };
+
return (
-
+
{(triggerRef) => (
-
+
)}
+ setMenuAnchor(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
+ isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+
+
+ }
+ />
{settings && (
)}
+ {switcherAnchor && (
+ {
+ setMenuAnchor(undefined);
+ setSwitcherAnchor(undefined);
+ }}
+ />
+ )}
);
}
diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx
index f7d343d0..93c3f1a5 100644
--- a/src/app/pages/client/sidebar/SpaceTabs.tsx
+++ b/src/app/pages/client/sidebar/SpaceTabs.tsx
@@ -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(
({ 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);
diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx
index e20d1246..bbd5d489 100644
--- a/src/app/pages/client/space/Space.tsx
+++ b/src/app/pages/client/space/Space.tsx
@@ -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(({ 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);
diff --git a/src/app/state/callPreferences.ts b/src/app/state/callPreferences.ts
index a8e82067..8e412796 100644
--- a/src/app/state/callPreferences.ts
+++ b/src/app/state/callPreferences.ts
@@ -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;
+export type CallPreferencesAtom = WritableAtom;
+
+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(
storeKey,
@@ -37,3 +36,7 @@ export const makeCallPreferencesAtom = (userId: string): CallPreferencesAtom =>
return callPreferencesAtom;
};
+
+export const clearCallPreferenceStore = (userId: string) => {
+ localStorage.removeItem(getStoreKey(userId));
+};
diff --git a/src/app/state/closedLobbyCategories.ts b/src/app/state/closedLobbyCategories.ts
index 40ecd163..0b72c2c9 100644
--- a/src/app/state/closedLobbyCategories.ts
+++ b/src/app/state/closedLobbyCategories.ts
@@ -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>(
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));
+};
diff --git a/src/app/state/closedNavCategories.ts b/src/app/state/closedNavCategories.ts
index ea61cb2e..d2a804c1 100644
--- a/src/app/state/closedNavCategories.ts
+++ b/src/app/state/closedNavCategories.ts
@@ -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>(
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));
+};
diff --git a/src/app/state/hooks/settings.ts b/src/app/state/hooks/settings.ts
index d90c7664..56994607 100644
--- a/src/app/state/hooks/settings.ts
+++ b/src/app/state/hooks/settings.ts
@@ -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(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 =
| Settings[K]
| ((s: Settings[K]) => Settings[K]);
-export const useSetSetting = (settingsAtom: typeof sAtom, key: K) => {
+export const useSetSetting = (key: K) => {
+ const settingsAtom = useSettingsAtom();
+
const setterAtom = useMemo(
() =>
atom], undefined>(null, (get, set, value) => {
@@ -22,12 +43,15 @@ export const useSetSetting = (settingsAtom: typeof sAt
};
export const useSetting = (
- settingsAtom: typeof sAtom,
key: K
): [Settings[K], ReturnType>] => {
+ 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];
};
diff --git a/src/app/state/navToActivePath.ts b/src/app/state/navToActivePath.ts
index a7405898..d4d4c18b 100644
--- a/src/app/state/navToActivePath.ts
+++ b/src/app/state/navToActivePath.ts
@@ -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';
diff --git a/src/app/state/openedSidebarFolder.ts b/src/app/state/openedSidebarFolder.ts
index 70373b3b..5d18c217 100644
--- a/src/app/state/openedSidebarFolder.ts
+++ b/src/app/state/openedSidebarFolder.ts
@@ -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>(
storeKey,
@@ -64,3 +63,7 @@ export const makeOpenedSidebarFolderAtom = (userId: string): OpenedSidebarFolder
return openedSidebarFolderAtom;
};
+
+export const clearOpenedSidebarFolderStore = (userId: string) => {
+ localStorage.removeItem(getStoreKey(userId));
+};
diff --git a/src/app/state/sessions.ts b/src/app/state/sessions.ts
index a9c34ce9..4ffc1025 100644
--- a/src/app/state/sessions.ts
+++ b/src/app/state/sessions.ts
@@ -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(
+ 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(
+ (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(
-// 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(
-// (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(undefined);
diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts
index 9fbad4ff..6849aa7b 100644
--- a/src/app/state/settings.ts
+++ b/src/app/state/settings.ts
@@ -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(
+// STORAGE_KEY,
+// (key) => getLocalStorageItem(key, defaultSettings),
+// (key, settings) => {
+// setLocalStorageItem(key, settings);
+// }
+// );
+
+export type SettingsAtom = WritableAtom;
+
+export const makeSettingsAtom = (userId: string): SettingsAtom => {
+ const settingsAtom = atomWithLocalStorage(
+ getStoreKey(userId),
+ (key) => getLocalStorageItem(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(getSettings());
-export const settingsAtom = atom(
- (get) => get(baseSettings),
- (get, set, update) => {
- set(baseSettings, update);
- setSettings(update);
- }
-);
diff --git a/src/app/state/spaceRooms.ts b/src/app/state/spaceRooms.ts
index ab6584e7..d8b827c4 100644
--- a/src/app/state/spaceRooms.ts
+++ b/src/app/state/spaceRooms.ts
@@ -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';
diff --git a/src/app/state/typingMembers.ts b/src/app/state/typingMembers.ts
index 5ff57af6..1aa7104a 100644
--- a/src/app/state/typingMembers.ts
+++ b/src/app/state/typingMembers.ts
@@ -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;
@@ -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) => {
diff --git a/src/app/state/utils/atomWithLocalStorage.ts b/src/app/state/utils/atomWithLocalStorage.ts
index f17d3a3d..0f5ee96c 100644
--- a/src/app/state/utils/atomWithLocalStorage.ts
+++ b/src/app/state/utils/atomWithLocalStorage.ts
@@ -1,20 +1,5 @@
import { atom } from 'jotai';
-export const getLocalStorageItem = (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 = (key: string, value: T) => {
- localStorage.setItem(key, JSON.stringify(value));
-};
-
export type GetLocalStorageItem = (key: string) => T;
export type SetLocalStorageItem = (key: string, value: T) => void;
@@ -39,7 +24,7 @@ export const atomWithLocalStorage = (
};
};
- const localStorageAtom = atom(
+ const localStorageAtom = atom(
(get) => get(baseAtom),
(get, set, newValue) => {
set(baseAtom, newValue);
diff --git a/src/client/LogoutManager.ts b/src/client/LogoutManager.ts
index 220442e3..0ee59b59 100644
--- a/src/client/LogoutManager.ts
+++ b/src/client/LogoutManager.ts
@@ -40,7 +40,6 @@ export class LogoutManager implements Disposable {
private async clearStoresAndRefresh(): Promise {
await this.deleteStorage();
- window.localStorage.clear();
window.location.reload();
}
diff --git a/src/client/context.ts b/src/client/context.ts
index 0507e1a0..7d043cb6 100644
--- a/src/client/context.ts
+++ b/src/client/context.ts
@@ -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';
diff --git a/src/client/crypto.ts b/src/client/crypto.ts
index fd92ab26..7102b494 100644
--- a/src/client/crypto.ts
+++ b/src/client/crypto.ts
@@ -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;
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 => {
+export const deleteClientCryptoStorage = async (session: Session): Promise => {
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);
+ }
+ }
};
diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts
deleted file mode 100644
index d11dd484..00000000
--- a/src/client/initMatrix.ts
+++ /dev/null
@@ -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 => {
- 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();
-};
diff --git a/src/client/initialize.ts b/src/client/initialize.ts
index d857f4c6..fa08e7a7 100644
--- a/src/client/initialize.ts
+++ b/src/client/initialize.ts
@@ -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 => {
+export type InitializeClientResult = {
+ ct: ClientContext;
+ dispose: () => void;
+};
+export const initializeClient = async (session: Session): Promise => {
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
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
});
disposer.collect(
+ disconnectStorage,
+ closeMachine,
megolmBackup,
decryption,
syncObservable,
@@ -172,5 +184,8 @@ export const initializeClient = async (session: Session): Promise
syncResponseManager
);
- return ct;
+ return {
+ ct,
+ dispose: () => disposer.run(),
+ };
};
diff --git a/src/client/localStorage.ts b/src/client/localStorage.ts
new file mode 100644
index 00000000..3a593285
--- /dev/null
+++ b/src/client/localStorage.ts
@@ -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 = (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 = (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);
+};
diff --git a/src/client/sessions.ts b/src/client/sessions.ts
new file mode 100644
index 00000000..8fd8d8f0
--- /dev/null
+++ b/src/client/sessions.ts
@@ -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(CLIENT_SESSIONS_STORE_KEY, []);
+
+ return sessions.some((s) => s.userId === userId);
+};
+
+export const removeSession = (session: Session): void => {
+ const sessions = getLocalStorageItem(CLIENT_SESSIONS_STORE_KEY, []);
+
+ const newSessions = sessions.filter((s) => s.userId === session.userId);
+
+ setLocalStorageItem(CLIENT_SESSIONS_STORE_KEY, newSessions);
+};
diff --git a/src/client/storage/index.ts b/src/client/storage/index.ts
index e79d3e63..6ca0f8a7 100644
--- a/src/client/storage/index.ts
+++ b/src/client/storage/index.ts
@@ -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;
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);
};
diff --git a/src/index.tsx b/src/index.tsx
index 6cc0ca38..89c650b4 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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 = () => {
diff --git a/src/sw-session.ts b/src/sw-session.ts
deleted file mode 100644
index 4b2ec055..00000000
--- a/src/sw-session.ts
+++ /dev/null
@@ -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,
- });
-}
diff --git a/src/sw.ts b/src/sw.ts
index 69293b1d..4eb0c63e 100644
--- a/src/sw.ts
+++ b/src/sw.ts
@@ -8,69 +8,30 @@ type SessionInfo = {
baseUrl: string;
};
-/**
- * Store session per client (tab)
- */
-const sessions = new Map();
+let _requestId = 0;
+const getRequestId = () => {
+ _requestId += 1;
+ return _requestId;
+};
-const clientToResolve = new Map void>();
-const clientToSessionPromise = new Map>();
+const pendingSessionRequests = new Map 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 {
- 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 {
+async function requestSession(clientId: string): Promise {
const client = await self.clients.get(clientId);
if (!client) return undefined;
- const sessionPromise = requestSession(client);
+ const id = getRequestId();
+
+ const sessionPromise = new Promise((resolve) => {
+ pendingSessionRequests.set(id, resolve);
+ client.postMessage({ type: 'session-request', id });
+ });
const timeout = new Promise((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));
}