1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from "rxjs";
4 import { retryWhen, delay, take } from 'rxjs/operators';
5 import { WebSocketService, UserService } from '../services';
6 import { UserOperation, GetRepliesForm, GetRepliesResponse, SortType, GetSiteResponse, Comment} from '../interfaces';
7 import { msgOp } from '../utils';
8 import { version } from '../version';
10 interface NavbarState {
13 expandUserDropdown: boolean;
14 replies: Array<Comment>,
20 export class Navbar extends Component<any, NavbarState> {
21 private wsSub: Subscription;
22 private userSub: Subscription;
23 emptyState: NavbarState = {
24 isLoggedIn: (UserService.Instance.user !== undefined),
29 expandUserDropdown: false,
33 constructor(props: any, context: any) {
34 super(props, context);
35 this.state = this.emptyState;
36 this.handleOverviewClick = this.handleOverviewClick.bind(this);
38 this.keepFetchingReplies();
40 // Subscribe to user changes
41 this.userSub = UserService.Instance.sub.subscribe(user => {
42 this.state.isLoggedIn = user.user !== undefined;
43 this.state.unreadCount = user.unreadCount;
44 this.requestNotificationPermission();
45 this.setState(this.state);
48 this.wsSub = WebSocketService.Instance.subject
49 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
51 (msg) => this.parseMessage(msg),
52 (err) => console.error(err),
53 () => console.log('complete')
56 if (this.state.isLoggedIn) {
57 this.requestNotificationPermission();
60 WebSocketService.Instance.getSite();
65 <div>{this.navbar()}</div>
69 componentWillUnmount() {
70 this.wsSub.unsubscribe();
71 this.userSub.unsubscribe();
74 // TODO class active corresponding to current page
77 <nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3">
78 <Link title={version} class="navbar-brand" to="/">
79 <svg class="icon mr-2 mouse-icon"><use xlinkHref="#icon-mouse"></use></svg>
82 <button class="navbar-toggler" type="button" onClick={linkEvent(this, this.expandNavbar)}>
83 <span class="navbar-toggler-icon"></span>
85 <div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
86 <ul class="navbar-nav mr-auto">
88 <Link class="nav-link" to="/communities">Communities</Link>
91 <Link class="nav-link" to="/search">Search</Link>
94 <Link class="nav-link" to={{pathname: '/create_post', state: { prevPath: this.currentLocation }}}>Create Post</Link>
97 <Link class="nav-link" to="/create_community">Create Community</Link>
100 <ul class="navbar-nav ml-auto mr-2">
101 {this.state.isLoggedIn ?
104 <li className="nav-item">
105 <Link class="inbox nav-link" to="/inbox">
106 <svg class="icon"><use xlinkHref="#icon-mail"></use></svg>
107 {this.state.unreadCount> 0 && <span class="ml-1 badge badge-light">{this.state.unreadCount}</span>}
111 <li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}>
112 <a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button">
113 {UserService.Instance.user.username}
115 <div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}>
116 <a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}>Overview</a>
117 <a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a>
122 <Link class="nav-link" to="/login">Login / Sign up</Link>
130 expandUserDropdown(i: Navbar) {
131 i.state.expandUserDropdown = !i.state.expandUserDropdown;
135 handleLogoutClick(i: Navbar) {
136 i.state.expandUserDropdown = false;
137 UserService.Instance.logout();
138 i.context.router.history.push('/');
141 handleOverviewClick(i: Navbar) {
142 i.state.expandUserDropdown = false;
144 let userPage = `/u/${UserService.Instance.user.username}`;
145 i.context.router.history.push(userPage);
148 expandNavbar(i: Navbar) {
149 i.state.expanded = !i.state.expanded;
153 parseMessage(msg: any) {
154 let op: UserOperation = msgOp(msg);
156 if (msg.error == "Not logged in.") {
157 UserService.Instance.logout();
161 } else if (op == UserOperation.GetReplies) {
162 let res: GetRepliesResponse = msg;
163 let unreadReplies = res.replies.filter(r => !r.read);
164 if (unreadReplies.length > 0 && this.state.fetchCount > 1 &&
165 (JSON.stringify(this.state.replies) !== JSON.stringify(unreadReplies))) {
166 this.notify(unreadReplies);
169 this.state.replies = unreadReplies;
170 this.sendRepliesCount(res);
171 } else if (op == UserOperation.GetSite) {
172 let res: GetSiteResponse = msg;
175 this.state.siteName = res.site.name;
176 WebSocketService.Instance.site = res.site;
177 this.setState(this.state);
182 keepFetchingReplies() {
184 setInterval(() => this.fetchReplies(), 30000);
188 if (this.state.isLoggedIn) {
189 let repliesForm: GetRepliesForm = {
190 sort: SortType[SortType.New],
195 WebSocketService.Instance.getReplies(repliesForm);
196 this.state.fetchCount++;
200 get currentLocation() {
201 return this.context.router.history.location.pathname;
204 sendRepliesCount(res: GetRepliesResponse) {
205 UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: res.replies.filter(r => !r.read).length});
208 requestNotificationPermission() {
209 if (UserService.Instance.user) {
210 document.addEventListener('DOMContentLoaded', function () {
212 alert('Desktop notifications not available in your browser. Try Chromium.');
216 if (Notification.permission !== 'granted')
217 Notification.requestPermission();
222 notify(replies: Array<Comment>) {
223 let recentReply = replies[0];
224 if (Notification.permission !== 'granted')
225 Notification.requestPermission();
227 var notification = new Notification(`${replies.length} Unread Messages`, {
228 icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
229 body: `${recentReply.creator_name}: ${recentReply.content}`
232 notification.onclick = () => {
233 this.context.router.history.push(`/post/${recentReply.post_id}/comment/${recentReply.id}`);