import { Component, createRef, linkEvent, RefObject } from "inferno"; import { Link } from "inferno-router"; import { CommentResponse, CommentView, GetPersonMentions, GetPersonMentionsResponse, GetPrivateMessages, GetReplies, GetRepliesResponse, GetReportCount, GetReportCountResponse, GetSiteResponse, PrivateMessageResponse, PrivateMessagesResponse, PrivateMessageView, SortType, UserOperation, } from "lemmy-js-client"; import { Subscription } from "rxjs"; import { i18n } from "../../i18next"; import { UserService, WebSocketService } from "../../services"; import { authField, donateLemmyUrl, fetchLimit, getLanguage, isBrowser, notifyComment, notifyPrivateMessage, numToSI, setTheme, showAvatars, toast, wsClient, wsJsonToRes, wsSubscribe, wsUserOp, } from "../../utils"; import { Icon } from "../common/icon"; import { PictrsImage } from "../common/pictrs-image"; interface NavbarProps { site_res: GetSiteResponse; } interface NavbarState { isLoggedIn: boolean; expanded: boolean; replies: CommentView[]; mentions: CommentView[]; messages: PrivateMessageView[]; unreadInboxCount: number; unreadReportCount: number; searchParam: string; toggleSearch: boolean; showDropdown: boolean; onSiteBanner?(url: string): any; } export class Navbar extends Component { private wsSub: Subscription; private userSub: Subscription; private unreadInboxCountSub: Subscription; private unreadReportCountSub: Subscription; private searchTextField: RefObject; emptyState: NavbarState = { isLoggedIn: !!this.props.site_res.my_user, unreadInboxCount: 0, unreadReportCount: 0, replies: [], mentions: [], messages: [], expanded: false, searchParam: "", toggleSearch: false, showDropdown: false, }; subscription: any; constructor(props: any, context: any) { super(props, context); this.state = this.emptyState; this.parseMessage = this.parseMessage.bind(this); this.subscription = wsSubscribe(this.parseMessage); } componentDidMount() { // Subscribe to jwt changes if (isBrowser()) { this.websocketEvents(); this.searchTextField = createRef(); console.log(`isLoggedIn = ${this.state.isLoggedIn}`); // On the first load, check the unreads if (this.state.isLoggedIn == false) { // setTheme(data.my_user.theme, true); // i18n.changeLanguage(getLanguage()); // i18n.changeLanguage('de'); } else { this.requestNotificationPermission(); WebSocketService.Instance.send( wsClient.userJoin({ auth: authField(), }) ); this.fetchUnreads(); } this.userSub = UserService.Instance.jwtSub.subscribe(res => { // A login if (res !== undefined) { this.requestNotificationPermission(); WebSocketService.Instance.send( wsClient.getSite({ auth: authField() }) ); } else { this.setState({ isLoggedIn: false }); } }); // Subscribe to unread count changes this.unreadInboxCountSub = UserService.Instance.unreadInboxCountSub.subscribe(res => { this.setState({ unreadInboxCount: res }); }); // Subscribe to unread report count changes this.unreadReportCountSub = UserService.Instance.unreadReportCountSub.subscribe(res => { this.setState({ unreadReportCount: res }); }); } } componentWillUnmount() { this.wsSub.unsubscribe(); this.userSub.unsubscribe(); this.unreadInboxCountSub.unsubscribe(); this.unreadReportCountSub.unsubscribe(); } updateUrl() { const searchParam = this.state.searchParam; this.setState({ searchParam: "" }); this.setState({ toggleSearch: false }); this.setState({ showDropdown: false, expanded: false }); if (searchParam === "") { this.context.router.history.push(`/search/`); } else { const searchParamEncoded = encodeURIComponent(searchParam); this.context.router.history.push( `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1` ); } } render() { return this.navbar(); } // TODO class active corresponding to current page navbar() { let myUserInfo = UserService.Instance.myUserInfo || this.props.site_res.my_user; let person = myUserInfo?.local_user_view.person; return ( ); } expandNavbar(i: Navbar) { i.state.expanded = !i.state.expanded; i.setState(i.state); } handleSearchParam(i: Navbar, event: any) { i.state.searchParam = event.target.value; i.setState(i.state); } handleSearchSubmit(i: Navbar, event: any) { event.preventDefault(); i.updateUrl(); } handleSearchBtn(i: Navbar, event: any) { event.preventDefault(); i.setState({ toggleSearch: true }); i.searchTextField.current.focus(); const offsetWidth = i.searchTextField.current.offsetWidth; if (i.state.searchParam && offsetWidth > 100) { i.updateUrl(); } } handleSearchBlur(i: Navbar, event: any) { if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) { i.state.toggleSearch = false; i.setState(i.state); } } handleLogoutClick(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); UserService.Instance.logout(); window.location.href = "/"; location.reload(); } handleGotoSettings(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push("/settings"); } handleGotoProfile(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push( `/u/${UserService.Instance.myUserInfo.local_user_view.person.name}` ); } handleGotoCreatePost(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push("/create_post", { prevPath: i.currentLocation, }); } handleGotoCreateCommunity(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push(`/create_community`); } handleGotoCommunities(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push(`/communities`); } handleGotoHome(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push(`/`); } handleGotoInbox(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push(`/inbox`); } handleGotoReports(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push(`/reports`); } handleGotoAdmin(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push(`/admin`); } handleGotoLogin(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push(`/login`); } handleGotoSignup(i: Navbar) { i.setState({ showDropdown: false, expanded: false }); i.context.router.history.push(`/signup`); } handleShowDropdown(i: Navbar) { i.state.showDropdown = !i.state.showDropdown; i.setState(i.state); } parseMessage(msg: any) { let op = wsUserOp(msg); console.log(msg); if (msg.error) { if (msg.error == "not_logged_in") { UserService.Instance.logout(); location.reload(); } return; } else if (msg.reconnect) { console.log(i18n.t("websocket_reconnected")); WebSocketService.Instance.send( wsClient.userJoin({ auth: authField(), }) ); this.fetchUnreads(); } else if (op == UserOperation.GetReplies) { let data = wsJsonToRes(msg).data; let unreadReplies = data.replies.filter(r => !r.comment.read); this.state.replies = unreadReplies; this.state.unreadInboxCount = this.calculateUnreadInboxCount(); this.setState(this.state); this.sendUnreadCount(); } else if (op == UserOperation.GetPersonMentions) { let data = wsJsonToRes(msg).data; let unreadMentions = data.mentions.filter(r => !r.comment.read); this.state.mentions = unreadMentions; this.state.unreadInboxCount = this.calculateUnreadInboxCount(); this.setState(this.state); this.sendUnreadCount(); } else if (op == UserOperation.GetPrivateMessages) { let data = wsJsonToRes(msg).data; let unreadMessages = data.private_messages.filter( r => !r.private_message.read ); this.state.messages = unreadMessages; this.state.unreadInboxCount = this.calculateUnreadInboxCount(); this.setState(this.state); this.sendUnreadCount(); } else if (op == UserOperation.GetReportCount) { let data = wsJsonToRes(msg).data; this.state.unreadReportCount = data.post_reports + data.comment_reports; this.setState(this.state); this.sendReportUnread(); } else if (op == UserOperation.GetSite) { // This is only called on a successful login let data = wsJsonToRes(msg).data; console.log(data.my_user); UserService.Instance.myUserInfo = data.my_user; setTheme( UserService.Instance.myUserInfo.local_user_view.local_user.theme ); i18n.changeLanguage(getLanguage()); this.state.isLoggedIn = true; this.setState(this.state); } else if (op == UserOperation.CreateComment) { let data = wsJsonToRes(msg).data; if (this.state.isLoggedIn) { if ( data.recipient_ids.includes( UserService.Instance.myUserInfo.local_user_view.local_user.id ) ) { this.state.replies.push(data.comment_view); this.state.unreadInboxCount++; this.setState(this.state); this.sendUnreadCount(); notifyComment(data.comment_view, this.context.router); } } } else if (op == UserOperation.CreatePrivateMessage) { let data = wsJsonToRes(msg).data; if (this.state.isLoggedIn) { if ( data.private_message_view.recipient.id == UserService.Instance.myUserInfo.local_user_view.person.id ) { this.state.messages.push(data.private_message_view); this.state.unreadInboxCount++; this.setState(this.state); this.sendUnreadCount(); notifyPrivateMessage(data.private_message_view, this.context.router); } } } } fetchUnreads() { // TODO we should just add a count call to the API for these, because this is a limited fetch, // and it shouldn't have to fetch the actual content if (this.currentLocation !== "/inbox") { console.log("Fetching inbox unreads..."); let repliesForm: GetReplies = { sort: SortType.New, unread_only: true, page: 1, limit: fetchLimit, auth: authField(), }; let personMentionsForm: GetPersonMentions = { sort: SortType.New, unread_only: true, page: 1, limit: fetchLimit, auth: authField(), }; let privateMessagesForm: GetPrivateMessages = { unread_only: true, page: 1, limit: fetchLimit, auth: authField(), }; WebSocketService.Instance.send(wsClient.getReplies(repliesForm)); WebSocketService.Instance.send( wsClient.getPersonMentions(personMentionsForm) ); WebSocketService.Instance.send( wsClient.getPrivateMessages(privateMessagesForm) ); } console.log("Fetching reports..."); let reportCountForm: GetReportCount = { auth: authField(), }; WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm)); } get currentLocation() { return this.context.router.history.location.pathname; } sendUnreadCount() { UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount); } sendReportUnread() { UserService.Instance.unreadReportCountSub.next( this.state.unreadReportCount ); } calculateUnreadInboxCount(): number { return ( this.state.replies.filter(r => !r.comment.read).length + this.state.mentions.filter(r => !r.comment.read).length + this.state.messages.filter(r => !r.private_message.read).length ); } get canAdmin(): boolean { return ( UserService.Instance.myUserInfo && this.props.site_res.admins .map(a => a.person.id) .includes(UserService.Instance.myUserInfo.local_user_view.person.id) ); } get canCreateCommunity(): boolean { let adminOnly = this.props.site_res.site_view?.site.community_creation_admin_only; return !adminOnly || this.canAdmin; } /// Listens for some websocket errors websocketEvents() { let msg = i18n.t("websocket_disconnected"); WebSocketService.Instance.closeEventListener(() => { console.error(msg); }); } requestNotificationPermission() { if (UserService.Instance.myUserInfo) { document.addEventListener("DOMContentLoaded", function () { if (!Notification) { toast(i18n.t("notifications_error"), "danger"); return; } if (Notification.permission !== "granted") Notification.requestPermission(); }); } } }