Skip to content

Commit 594e2bf

Browse files
committed
wip topbar; added native notifications; added socket client
1 parent dee4dc1 commit 594e2bf

16 files changed

+196
-117
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
### Screenshots (work in progress)
1515
![screen](./screenshot1.png)
1616
![screen2](./screenshot2.png)
17-
![screen3](./screenshot3.png)
1817

1918
## Installing
2019

assets/user-placeholder.jpeg

-132 KB
Binary file not shown.

screenshot1.png

166 KB
Loading

screenshot2.png

147 KB
Loading

screenshot3.png

-598 KB
Binary file not shown.

src/App.tsx

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,28 @@
1-
import { Center, Spinner } from '@chakra-ui/react';
2-
import { useEffect, useState } from 'react';
1+
import { useEffect } from 'react';
32
import { Layout } from './components/layout/Layout';
4-
import stackoverflow from './unitls/stackexchange-api';
53
import { useNavigate } from 'react-router-dom';
4+
import { UserProvider } from './contexts/use-user';
5+
import { AppSpinner } from './components/layout/AppSpinner';
66

77
export function App() {
88
const navigate = useNavigate();
9-
const [isAuthorized, setIsAuthorized] = useState(false);
109

1110
useEffect(() => {
12-
window.Main.on('stackexchange:on-auth', ({ token }: any) => {
13-
localStorage.setItem('token', token);
14-
setIsAuthorized(true);
15-
16-
stackoverflow.getLoggedInUser().then((user) => {
17-
// console.log(user, 2);
18-
});
19-
});
20-
2111
document.addEventListener('paste', (e) => {
2212
const clipboardData = e.clipboardData;
2313
const pastedText = clipboardData?.getData('text');
2414

25-
// FIXME on Question page when pastin new url hash is changing, but rerender missing
2615
if (pastedText && pastedText.startsWith('https://stackoverflow.com/questions/')) {
2716
const questionId = pastedText.replace('https://stackoverflow.com/questions/', '').split('/')[0];
2817

29-
navigate(`/questions/${questionId}`, { replace: true });
18+
navigate(`/questions/${questionId}`);
3019
}
3120
});
3221
}, []);
3322

3423
return (
35-
<>
36-
{isAuthorized ? (
37-
<Layout />
38-
) : (
39-
<Center h="100vh">
40-
<Spinner />
41-
</Center>
42-
)}
43-
</>
24+
<UserProvider LoadingComponent={<AppSpinner />}>
25+
<Layout />
26+
</UserProvider>
4427
);
4528
}

src/components/comments/CommentForm.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { Box, HStack, Image, Input, Link } from '@chakra-ui/react';
2-
import UserPlaceholder from '../../../assets/user-placeholder.jpeg';
32
import { useState } from 'react';
3+
import { useUser } from '../../contexts/use-user';
44

55
type Props = {
66
olderCommentsCount?: number;
77
};
88

99
export function CommentForm({ olderCommentsCount }: Props) {
10+
const user = useUser();
11+
1012
const [view, setView] = useState<'form' | 'link'>('form');
1113

1214
if (view === 'link') {
@@ -19,7 +21,7 @@ export function CommentForm({ olderCommentsCount }: Props) {
1921
return (
2022
<HStack align="flex-start" fontSize="13px">
2123
<Box flexBasis="40px" flexShrink={0}>
22-
<Image src={UserPlaceholder} boxSize="24px" objectFit="cover" borderRadius="3px" />
24+
<Image src={user.data.profile_image} boxSize="24px" objectFit="cover" borderRadius="3px" />
2325
</Box>
2426
<Box w="100%">
2527
<Input size="xs" placeholder="Your comment..." />

src/components/layout/AppSpinner.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Center, Spinner } from '@chakra-ui/react';
2+
3+
export function AppSpinner() {
4+
return (
5+
<Center h="100vh">
6+
<Spinner />
7+
</Center>
8+
);
9+
}

src/components/layout/Layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { SponsorWidget } from './SponsorWidget';
1616
export function Layout() {
1717
return (
1818
<>
19-
<Center bgColor="gray.800" h="40px" css={{ WebkitAppRegion: 'drag' }}>
19+
<Center bgColor="gray.800" h="40px" sx={{ WebkitAppRegion: 'drag' }}>
2020
<Box justifySelf="flex-start" flex={1} />
2121
<Box flex={1}>
2222
<SearchBar />

src/components/layout/NavItem.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ export function NavItem({ children, count, to }: Props) {
3030
spacing="6px"
3131
>
3232
{children}
33-
{count && <Badge display="block" style={{ marginLeft: 'auto' }}>{count}</Badge>}
33+
{count && (
34+
<Badge display="block" style={{ marginLeft: 'auto' }}>
35+
{count}
36+
</Badge>
37+
)}
3438
</HStack>
3539
</RouterLink>
3640
);

src/components/layout/SponsorWidget.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { Box, BoxProps, Heading, HStack, Text } from '@chakra-ui/react';
22
import { IoIosHeart } from 'react-icons/io';
3+
import { useNavigate } from 'react-router-dom';
34

45
export function SponsorWidget(props: BoxProps) {
6+
const navigate = useNavigate();
7+
8+
function cl() {
9+
navigate('/questions/30340815');
10+
}
11+
512
return (
6-
<Box bgColor="whiteAlpha.200" color="whiteAlpha.900" fontSize="13px" borderRadius="5px" p="8px" mx="8px" {...props}>
13+
<Box bgColor="whiteAlpha.200" color="whiteAlpha.900" fontSize="13px" borderRadius="5px" p="8px" mx="8px" {...props} onClick={cl}>
714
<Heading fontSize="11px" textTransform="uppercase" mb="6px">
815
<HStack spacing="4px">
916
<Text color="red.500">
Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { Image, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
2-
import UserPlaceholder from '../../../assets/user-placeholder.jpeg';
1+
import { Box, Center, HStack, Image, Menu, MenuButton, MenuDivider, MenuItem, MenuList, Text } from '@chakra-ui/react';
32
import stackoverflow from '../../unitls/stackexchange-api';
3+
import { useUser } from '../../contexts/use-user';
4+
import { BsInboxFill } from 'react-icons/bs';
5+
import { kFormatter } from '../../unitls/k-formatter';
46

57
export function UserMenuDropdown() {
8+
const user = useUser();
9+
610
function logout() {
711
stackoverflow.get(`access-tokens/${localStorage.token}/invalidate`).then(() => {
812
// window.Main.logout();
@@ -13,13 +17,33 @@ export function UserMenuDropdown() {
1317
}
1418

1519
return (
16-
<Menu>
17-
<MenuButton display="flex" ml="auto" mr="10px">
18-
<Image src={UserPlaceholder} boxSize="25px" objectFit="cover" borderRadius="5px" />
19-
</MenuButton>
20-
<MenuList>
21-
<MenuItem onClick={logout}>Logout</MenuItem>
22-
</MenuList>
23-
</Menu>
20+
<HStack justify="end" align="stretch" mr="16px" spacing={0} sx={{ WebkitAppRegion: 'no-drag' }}>
21+
<Center px="8px" rounded="3px" _hover={{ color: 'whiteAlpha.700', bgColor: 'whiteAlpha.50' }} color="whiteAlpha.600">
22+
<Text fontSize="16px">
23+
<BsInboxFill />
24+
</Text>
25+
<Box boxSize="6px" bgColor="red.500" rounded="full" position="relative" ml="-1px" top="-6px" />
26+
</Center>
27+
<Center px="8px" rounded="3px" _hover={{ color: 'whiteAlpha.700', bgColor: 'whiteAlpha.50' }} color="whiteAlpha.600">
28+
<Text fontSize="12px" fontWeight="semibold">
29+
{kFormatter(user.data.reputation)}
30+
<Text as="span" ml="3px" px="3px" mt="1px" bgColor="green.500" color="whiteAlpha.800" rounded="2px">
31+
+25
32+
</Text>
33+
</Text>
34+
</Center>
35+
36+
<Menu>
37+
<MenuButton marginStart="10px !important">
38+
<Image src={user.data.profile_image} boxSize="25px" objectFit="cover" borderRadius="5px" />
39+
</MenuButton>
40+
<MenuList zIndex={200}>
41+
<MenuItem>Profile</MenuItem>
42+
<MenuItem command="⌘,">Settings</MenuItem>
43+
<MenuDivider />
44+
<MenuItem onClick={logout}>Logout</MenuItem>
45+
</MenuList>
46+
</Menu>
47+
</HStack>
2448
);
2549
}

src/contexts/use-user.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
2+
import { UserType } from '../interfaces/UserType';
3+
import stackoverflow from '../unitls/stackexchange-api';
4+
import { socketClient } from '../unitls/stackexchange-socket-client';
5+
6+
export type UserContextState = {
7+
data: UserType;
8+
token?: string;
9+
};
10+
11+
export const UserContext = createContext<UserContextState>({} as UserContextState);
12+
13+
type Props = {
14+
LoadingComponent: ReactNode;
15+
children: ReactNode;
16+
};
17+
18+
export const UserProvider = ({ children, LoadingComponent }: Props) => {
19+
const [isLoading, setIsLoading] = useState(true);
20+
const [user, setUser] = useState<UserType>({} as UserType);
21+
const [token, setToken] = useState<string>();
22+
23+
const sharedState: UserContextState = {
24+
data: user,
25+
token
26+
};
27+
28+
useEffect(() => {
29+
window.Main.on('stackexchange:on-auth', async ({ token }: any) => {
30+
const loggedInUser = await stackoverflow.getLoggedInUser();
31+
32+
socketClient.on(`1-${user.user_id}-reputation`, () => {
33+
new Notification('Reputation', { body: '+25' });
34+
});
35+
36+
socketClient.on(`${user.account_id}-inbox`, () => {
37+
new Notification('Inbox', { body: 'You got new message' });
38+
});
39+
40+
setToken(token);
41+
setUser(loggedInUser);
42+
setIsLoading(false);
43+
});
44+
}, []);
45+
46+
if (isLoading) {
47+
return <>{LoadingComponent}</>;
48+
}
49+
50+
return <UserContext.Provider value={sharedState}>{children}</UserContext.Provider>;
51+
};
52+
53+
export const useUser = () => {
54+
return useContext(UserContext);
55+
};

src/pages/QuestionDetailsPage.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { QuestionDetails } from '../components/posts/QuestionDetails';
1010
import { AnswerDetails } from '../components/posts/AnswerDetails';
1111
import { AnswerType } from '../interfaces/AnswerType';
1212
import { StickyAnswerForm } from '../components/posts/StickyAnswerForm';
13+
import { socketClient } from '../unitls/stackexchange-socket-client';
1314

1415
let tooltipTimerId: NodeJS.Timer;
1516

@@ -39,6 +40,14 @@ export function QuestionDetailsPage() {
3940
.then((response) => {
4041
setAnswers((response as any).items);
4142
});
43+
44+
socketClient.on(`1-question-${id}`, () => {
45+
new Notification('Question', { body: 'questions changed' });
46+
});
47+
48+
return () => {
49+
socketClient.off(`1-question-${id}`);
50+
};
4251
}, []);
4352

4453
function jumpToAnswers() {
@@ -111,7 +120,9 @@ export function QuestionDetailsPage() {
111120
</Stack>
112121
</Box>
113122
) : (
114-
<Text mb="32px" color="gray" mt="48px" textAlign="center" ref={answersRef}>There are no answers yet.</Text>
123+
<Text mb="32px" color="gray" mt="48px" textAlign="center" ref={answersRef}>
124+
There are no answers yet.
125+
</Text>
115126
)}
116127

117128
<StickyAnswerForm />

src/unitls/stackexchange-api.ts

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -38,79 +38,3 @@ const stackoverflow = {
3838
};
3939

4040
export default stackoverflow;
41-
42-
// export const logout = (token: string) => {
43-
// let logoutPromise = fetch(
44-
// buildStackOverflowUrl('apps/' + token + '/de-authenticate', {
45-
// key: 'bdFSxniGkNbU3E*jsj*28w(('
46-
// })
47-
// );
48-
//
49-
// // Remove acct cookies to logout user from SE
50-
// logoutPromise.then(() => {
51-
// window.Main.webContents.session.cookies.remove('https://stackexchange.com', 'acct', noop);
52-
// });
53-
//
54-
// return logoutPromise;
55-
// };
56-
57-
// class StackOverflowSocketClient {
58-
// constructor() {
59-
// this.subscribedActions = [];
60-
// this.unsubscribedActions = [];
61-
// this.callbacks = {};
62-
//
63-
// this.socketConnectionPromise = new Promise((socketConnectionPromiseResolve, socketConnectionPromiseReject) => {
64-
// const connection = new WebSocket('wss://qa.sockets.stackexchange.com');
65-
//
66-
// connection.onopen = function () {
67-
// socketConnectionPromiseResolve(connection);
68-
// };
69-
//
70-
// connection.onerror = socketConnectionPromiseReject;
71-
//
72-
// connection.onmessage = event => {
73-
// const response = JSON.parse(event.data);
74-
//
75-
// // Stack Overflow doesn't allow us to unsubscribe from actions we subscribed
76-
// // so emulate unsubscribing by ignoring onMessage event
77-
// if (this.unsubscribedActions.includes(response.action)) {
78-
// return;
79-
// }
80-
//
81-
// if (this.callbacks[response.action]) {
82-
// let json;
83-
//
84-
// try {
85-
// json = JSON.parse(response.data);
86-
// } catch (ignore) {}
87-
//
88-
// this.callbacks[response.action](json || response.data);
89-
// }
90-
// };
91-
// });
92-
// }
93-
//
94-
// on(action, callback) {
95-
// this.socketConnectionPromise.then(connection => {
96-
// if (!this.subscribedActions.includes(action)) {
97-
// connection.send(action);
98-
// this.subscribedActions.push(action);
99-
// this.callbacks[action] = callback;
100-
// }
101-
// });
102-
// }
103-
//
104-
// off(action) {
105-
// if (!this.unsubscribedActions.includes(action)) {
106-
// this.unsubscribedActions.push(action);
107-
//
108-
// const index = this.subscribedActions.indexOf(action);
109-
// if (index !== -1) {
110-
// this.subscribedActions.splice(index, 1);
111-
// }
112-
// }
113-
// }
114-
// }
115-
//
116-
// socketClient = new StackOverflowSocketClient;

0 commit comments

Comments
 (0)