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 Cinny Logo; +} + +type AuthLayoutProps = { + hideHeader?: boolean; + center?: boolean; + children?: ReactNode; +}; +export function AuthLayout({ hideHeader, center, children }: AuthLayoutProps) { + return ( + + + + + {!hideHeader && ( +
+ + + Cinny + +
+ )} + + {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) => ( + <> + + {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) => ( - <> - - {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) => ( + + ))} + + + + + + + + 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 Logo - 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 ( + setSessionUserId(userId)} + variant={currentSelected ? 'Success' : 'Surface'} + aria-pressed={currentSelected} + before={ + + } + /> + + } + after={ + + {server} + + } + > + + {displayName} + + + ); +} + +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, + }} + > + + {sessions.length > 0 && ( + <> + + + {sessions.map((session) => ( + + ))} + + + + + )} + + } + onClick={handleManageAccounts} + > + + Manage Accounts + + + + + + } + /> + ); +} 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 + + + } + onClick={handleOpenAccountSwitcher} + aria-pressed={!!switcherAnchor} + > + + Switch Accounts + + + + + + } + /> {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)); }