diff options
Diffstat (limited to 'packages/renderer/src/components/sidebar')
5 files changed, 366 insertions, 0 deletions
diff --git a/packages/renderer/src/components/sidebar/ServiceIcon.tsx b/packages/renderer/src/components/sidebar/ServiceIcon.tsx new file mode 100644 index 0000000..83b2a5f --- /dev/null +++ b/packages/renderer/src/components/sidebar/ServiceIcon.tsx | |||
@@ -0,0 +1,119 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import Badge from '@mui/material/Badge'; | ||
22 | import { styled, useTheme } from '@mui/material/styles'; | ||
23 | import { Service } from '@sophie/shared'; | ||
24 | import { observer } from 'mobx-react-lite'; | ||
25 | import React from 'react'; | ||
26 | |||
27 | const ServiceIconRoot = styled('div', { | ||
28 | name: 'ServiceIcon', | ||
29 | slot: 'Root', | ||
30 | })(({ theme }) => ({ | ||
31 | width: 36, | ||
32 | height: 36, | ||
33 | borderRadius: theme.shape.borderRadius, | ||
34 | background: 'currentColor', | ||
35 | display: 'flex', | ||
36 | justifyContent: 'center', | ||
37 | alignItems: 'center', | ||
38 | })); | ||
39 | |||
40 | const ServiceIconText = styled('div', { | ||
41 | name: 'ServiceIcon', | ||
42 | slot: 'Text', | ||
43 | })(({ theme }) => ({ | ||
44 | display: 'inline-block', | ||
45 | flex: 0, | ||
46 | fontSize: theme.typography.pxToRem(24), | ||
47 | color: theme.palette.primary.contrastText, | ||
48 | })); | ||
49 | |||
50 | const IndirectMessageBadge = styled(Badge)(({ theme }) => ({ | ||
51 | '& .MuiBadge-dot': { | ||
52 | // The indirect message badge floats ouside the icon in the middle. | ||
53 | top: '50%', | ||
54 | ...(theme.direction === 'ltr' | ||
55 | ? { | ||
56 | left: theme.spacing(-1), | ||
57 | } | ||
58 | : { | ||
59 | right: theme.spacing(-1), | ||
60 | }), | ||
61 | background: | ||
62 | theme.palette.mode === 'dark' | ||
63 | ? theme.palette.text.primary | ||
64 | : 'currentColor', | ||
65 | }, | ||
66 | })); | ||
67 | |||
68 | const DirectMessageBadge = styled(Badge)(({ theme }) => ({ | ||
69 | '& .MuiBadge-badge': { | ||
70 | // Move the badge closer to the icon so that even "99+" messages can fit in the sidebar. | ||
71 | ...(theme.direction === 'ltr' | ||
72 | ? { | ||
73 | right: theme.spacing(0.25), | ||
74 | } | ||
75 | : { | ||
76 | left: theme.spacing(0.25), | ||
77 | }), | ||
78 | top: theme.spacing(0.25), | ||
79 | // Set the badge apart from the icon with a shadow (a border would be too heavy). | ||
80 | boxShadow: theme.shadows[1], | ||
81 | // Add a bit more emphasis to the badge. | ||
82 | fontWeight: 700, | ||
83 | }, | ||
84 | })); | ||
85 | |||
86 | function ServiceIcon({ service }: { service: Service }): JSX.Element { | ||
87 | const { direction } = useTheme(); | ||
88 | const { | ||
89 | settings: { name }, | ||
90 | directMessageCount, | ||
91 | indirectMessageCount, | ||
92 | } = service; | ||
93 | |||
94 | return ( | ||
95 | <IndirectMessageBadge | ||
96 | badgeContent={indirectMessageCount} | ||
97 | variant="dot" | ||
98 | anchorOrigin={{ | ||
99 | vertical: 'top', | ||
100 | horizontal: direction === 'ltr' ? 'left' : 'right', | ||
101 | }} | ||
102 | > | ||
103 | <DirectMessageBadge | ||
104 | badgeContent={directMessageCount} | ||
105 | color="error" | ||
106 | anchorOrigin={{ | ||
107 | vertical: 'top', | ||
108 | horizontal: direction === 'ltr' ? 'right' : 'left', | ||
109 | }} | ||
110 | > | ||
111 | <ServiceIconRoot> | ||
112 | <ServiceIconText>{name.length > 0 ? name[0] : '?'}</ServiceIconText> | ||
113 | </ServiceIconRoot> | ||
114 | </DirectMessageBadge> | ||
115 | </IndirectMessageBadge> | ||
116 | ); | ||
117 | } | ||
118 | |||
119 | export default observer(ServiceIcon); | ||
diff --git a/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx b/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx new file mode 100644 index 0000000..3b47990 --- /dev/null +++ b/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx | |||
@@ -0,0 +1,92 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import Tab from '@mui/material/Tab'; | ||
22 | import Tabs from '@mui/material/Tabs'; | ||
23 | import { alpha, styled } from '@mui/material/styles'; | ||
24 | import { observer } from 'mobx-react-lite'; | ||
25 | import React from 'react'; | ||
26 | |||
27 | import { useStore } from '../StoreProvider'; | ||
28 | |||
29 | import ServiceIcon from './ServiceIcon'; | ||
30 | |||
31 | const ServiceSwitcherRoot = styled(Tabs, { | ||
32 | name: 'ServiceSwitcher', | ||
33 | slot: 'Root', | ||
34 | })(({ theme }) => ({ | ||
35 | '.MuiTabs-indicator': { | ||
36 | ...(theme.direction === 'ltr' | ||
37 | ? { | ||
38 | left: 0, | ||
39 | right: 'auto', | ||
40 | } | ||
41 | : { | ||
42 | left: 'auto', | ||
43 | right: 0, | ||
44 | }), | ||
45 | }, | ||
46 | })); | ||
47 | |||
48 | const ServiceSwitcherTab = styled(Tab, { | ||
49 | name: 'ServiceSwitcher', | ||
50 | slot: 'Tab', | ||
51 | })(({ theme }) => ({ | ||
52 | minWidth: 0, | ||
53 | transition: theme.transitions.create('background-color', { | ||
54 | duration: theme.transitions.duration.shortest, | ||
55 | }), | ||
56 | '&.Mui-selected': { | ||
57 | backgroundColor: | ||
58 | theme.palette.mode === 'dark' | ||
59 | ? alpha(theme.palette.text.primary, 0.12) | ||
60 | : alpha(theme.palette.primary.light, 0.24), | ||
61 | }, | ||
62 | })); | ||
63 | |||
64 | function ServiceSwitcher(): JSX.Element { | ||
65 | const store = useStore(); | ||
66 | const { | ||
67 | settings: { selectedService }, | ||
68 | services, | ||
69 | } = store; | ||
70 | |||
71 | return ( | ||
72 | <ServiceSwitcherRoot | ||
73 | variant="scrollable" | ||
74 | orientation="vertical" | ||
75 | value={selectedService === undefined ? false : selectedService.id} | ||
76 | onChange={(_event, newValue: string) => | ||
77 | store.setSelectedServiceId(newValue) | ||
78 | } | ||
79 | > | ||
80 | {services.map((service) => ( | ||
81 | <ServiceSwitcherTab | ||
82 | key={service.id} | ||
83 | value={service.id} | ||
84 | icon={<ServiceIcon service={service} />} | ||
85 | aria-label={service.settings.name} | ||
86 | /> | ||
87 | ))} | ||
88 | </ServiceSwitcherRoot> | ||
89 | ); | ||
90 | } | ||
91 | |||
92 | export default observer(ServiceSwitcher); | ||
diff --git a/packages/renderer/src/components/sidebar/Sidebar.tsx b/packages/renderer/src/components/sidebar/Sidebar.tsx new file mode 100644 index 0000000..80826ca --- /dev/null +++ b/packages/renderer/src/components/sidebar/Sidebar.tsx | |||
@@ -0,0 +1,57 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import Box from '@mui/material/Box'; | ||
22 | import { alpha } from '@mui/material/styles'; | ||
23 | import React from 'react'; | ||
24 | |||
25 | import ServiceSwitcher from './ServiceSwitcher'; | ||
26 | import ToggleDarkModeButton from './ToggleDarkModeButton'; | ||
27 | import ToggleLocationBarButton from './ToggleLocationBarButton'; | ||
28 | |||
29 | export default function Sidebar(): JSX.Element { | ||
30 | return ( | ||
31 | <Box | ||
32 | component="aside" | ||
33 | sx={(theme) => ({ | ||
34 | flex: 0, | ||
35 | display: 'flex', | ||
36 | flexDirection: 'column', | ||
37 | alignItems: 'center', | ||
38 | paddingY: 1, | ||
39 | gap: 1, | ||
40 | background: alpha(theme.palette.text.primary, 0.09), | ||
41 | backgroundClip: 'padding-box', | ||
42 | borderInlineEnd: `1px solid ${theme.palette.divider}`, | ||
43 | minWidth: 69, | ||
44 | })} | ||
45 | > | ||
46 | <ToggleLocationBarButton /> | ||
47 | <ServiceSwitcher /> | ||
48 | <Box | ||
49 | sx={{ | ||
50 | flex: 1, | ||
51 | WebkitAppRegion: 'drag', | ||
52 | }} | ||
53 | /> | ||
54 | <ToggleDarkModeButton /> | ||
55 | </Box> | ||
56 | ); | ||
57 | } | ||
diff --git a/packages/renderer/src/components/sidebar/ToggleDarkModeButton.tsx b/packages/renderer/src/components/sidebar/ToggleDarkModeButton.tsx new file mode 100644 index 0000000..164b066 --- /dev/null +++ b/packages/renderer/src/components/sidebar/ToggleDarkModeButton.tsx | |||
@@ -0,0 +1,43 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import DarkModeIcon from '@mui/icons-material/DarkMode'; | ||
22 | import LightModeIcon from '@mui/icons-material/LightMode'; | ||
23 | import IconButton from '@mui/material/IconButton'; | ||
24 | import { observer } from 'mobx-react-lite'; | ||
25 | import React from 'react'; | ||
26 | |||
27 | import { useStore } from '../StoreProvider'; | ||
28 | |||
29 | export default observer(() => { | ||
30 | const store = useStore(); | ||
31 | const { | ||
32 | shared: { shouldUseDarkColors }, | ||
33 | } = store; | ||
34 | |||
35 | return ( | ||
36 | <IconButton | ||
37 | aria-label="Toggle dark mode" | ||
38 | onClick={() => store.toggleDarkMode()} | ||
39 | > | ||
40 | {shouldUseDarkColors ? <LightModeIcon /> : <DarkModeIcon />} | ||
41 | </IconButton> | ||
42 | ); | ||
43 | }); | ||
diff --git a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx new file mode 100644 index 0000000..7e20598 --- /dev/null +++ b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx | |||
@@ -0,0 +1,55 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import IconChevronLeft from '@mui/icons-material/KeyboardDoubleArrowLeft'; | ||
22 | import IconChevronRight from '@mui/icons-material/KeyboardDoubleArrowRight'; | ||
23 | import CircularProgress from '@mui/material/CircularProgress'; | ||
24 | import IconButton from '@mui/material/IconButton'; | ||
25 | import { observer } from 'mobx-react-lite'; | ||
26 | import React from 'react'; | ||
27 | |||
28 | import { useStore } from '../StoreProvider'; | ||
29 | |||
30 | function ToggleLocationBarButton(): JSX.Element { | ||
31 | const store = useStore(); | ||
32 | const { | ||
33 | settings: { selectedService, showLocationBar }, | ||
34 | } = store; | ||
35 | |||
36 | let icon: JSX.Element; | ||
37 | if (selectedService?.state === 'loading') { | ||
38 | icon = <CircularProgress color="inherit" size="1.5rem" />; | ||
39 | } else { | ||
40 | icon = showLocationBar ? <IconChevronLeft /> : <IconChevronRight />; | ||
41 | } | ||
42 | |||
43 | return ( | ||
44 | <IconButton | ||
45 | aria-pressed={showLocationBar} | ||
46 | aria-controls="locationBar" | ||
47 | aria-label="Show location bar" | ||
48 | onClick={() => store.toggleLocationBar()} | ||
49 | > | ||
50 | {icon} | ||
51 | </IconButton> | ||
52 | ); | ||
53 | } | ||
54 | |||
55 | export default observer(ToggleLocationBarButton); | ||