Skip to content

Commit

Permalink
Notifications overlay
Browse files Browse the repository at this point in the history
  • Loading branch information
rafalp committed Apr 19, 2023
1 parent 786ae1b commit 558d572
Show file tree
Hide file tree
Showing 24 changed files with 461 additions and 130 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
NotificationsListError,
NotificationsListLoading,
} from "../NotificationsList"
import NotificationsDropdownBody from "./NotificationsDropdownBody"
import NotificationsDropdownLayout from "./NotificationsDropdownLayout"

export default class NotificationsDropdown extends React.Component {
constructor(props) {
Expand All @@ -24,7 +24,7 @@ export default class NotificationsDropdown extends React.Component {
}

render = () => (
<NotificationsDropdownBody
<NotificationsDropdownLayout
unread={this.state.unread}
showAll={() => this.setState({ unread: false })}
showUnread={() => this.setState({ unread: true })}
Expand All @@ -50,6 +50,6 @@ export default class NotificationsDropdown extends React.Component {
)
}}
</NotificationsFetch>
</NotificationsDropdownBody>
</NotificationsDropdownLayout>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ import classnames from "classnames"
import React from "react"
import { DropdownFooter, DropdownHeader, DropdownPills } from "../Dropdown"

export default function NotificationsDropdownBody({
export default function NotificationsDropdownLayout({
children,
showAll,
showUnread,
unread,
}) {
return (
<div className="notifications-dropdown-body">
<div className="notifications-dropdown-layout">
<DropdownHeader>
{pgettext("notifications title", "Notifications")}
</DropdownHeader>
<DropdownPills>
<NotificationsDropdownBodyPill active={!unread} onClick={showAll}>
<NotificationsDropdownLayoutPill active={!unread} onClick={showAll}>
{pgettext("notifications dropdown", "All")}
</NotificationsDropdownBodyPill>
<NotificationsDropdownBodyPill active={unread} onClick={showUnread}>
</NotificationsDropdownLayoutPill>
<NotificationsDropdownLayoutPill active={unread} onClick={showUnread}>
{pgettext("notifications dropdown", "Unread")}
</NotificationsDropdownBodyPill>
</NotificationsDropdownLayoutPill>
</DropdownPills>
{children}
<DropdownFooter>
Expand All @@ -34,7 +34,7 @@ export default function NotificationsDropdownBody({
)
}

function NotificationsDropdownBodyPill({ active, children, onClick }) {
function NotificationsDropdownLayoutPill({ active, children, onClick }) {
return (
<button
className={classnames("btn", {
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/components/NotificationsList/NotificationsList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import NotificationsListEmpty from "./NotificationsListEmpty"
import NotificationsListGroup from "./NotificationsListGroup"
import NotificationsListItem from "./NotificationsListItem"

export default function NotificationsList({ className, filter, items }) {
export default function NotificationsList({ filter, items }) {
return (
<NotificationsListGroup className={className}>
<NotificationsListGroup
className={
items.length > 0
? "notifications-list-ready"
: "notifications-list-pending"
}
>
{items.length === 0 && <NotificationsListEmpty filter={filter} />}
{items.map((notification) => (
<NotificationsListItem
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from "react"
import NotificationsListGroup from "./NotificationsListGroup"

export default function NotificationsListError({ className, error }) {
export default function NotificationsListError({ error }) {
console.log(error)

const detail = errorDetail(error)

return (
<NotificationsListGroup className={className}>
<NotificationsListGroup className="notifications-list-pending">
<li className="list-group-item notifications-list-error">
<div className="notifications-list-error-icon">
<span className="material-icon">notifications_off</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react"
import NotificationsListGroup from "./NotificationsListGroup"

export default function NotificationsListLoading({ className }) {
export default function NotificationsListLoading() {
return (
<NotificationsListGroup className={className}>
<NotificationsListGroup className="notifications-list-pending">
<li className="list-group-item notifications-list-loading">
<p className="notifications-list-loading-message">
{pgettext("notifications list", "Loading notifications...")}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from "react"
import { connect } from "react-redux"
import { close } from "../../reducers/notifications"
import NotificationsFetch from "../NotificationsFetch"
import {
NotificationsList,
NotificationsListError,
NotificationsListLoading,
} from "../NotificationsList"
import NotificationsOverlayLayout from "./NotificationsOverlayLayout"

class NotificationsOverlay extends React.Component {
constructor(props) {
super(props)

this.body = document.body

this.state = {
unread: false,
url: "",
}
}

getApiUrl() {
let url = misago.get("NOTIFICATIONS_API") + "?limit=20"
url += this.state.unread ? "&filter=unread" : ""
return url
}

componentDidUpdate(prevProps, prevState) {
if (prevProps.open !== this.props.open) {
if (this.props.open) {
this.body.classList.add("notifications-fullscreen")
} else {
this.body.classList.remove("notifications-fullscreen")
}
}
}

close = () => {
this.props.dispatch(close())
}

render = () => (
<NotificationsOverlayLayout
close={this.close}
unread={this.state.unread}
showAll={() => this.setState({ unread: false })}
showUnread={() => this.setState({ unread: true })}
>
<NotificationsFetch
filter={this.state.unread ? "unread" : "all"}
disabled={!this.props.open}
>
{({ data, loading, error }) => {
if (loading) {
return <NotificationsListLoading />
}

if (error) {
return <NotificationsListError error={error} />
}

return (
<NotificationsList
filter={this.state.unread ? "unread" : "all"}
items={data ? data.results : []}
/>
)
}}
</NotificationsFetch>
</NotificationsOverlayLayout>
)
}

function select(state) {
return { open: state.notifications.open }
}

const NotificationsOverlayConnected = connect(select)(NotificationsOverlay)

export default NotificationsOverlayConnected
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import classnames from "classnames"
import React from "react"
import { DropdownFooter, DropdownHeader, DropdownPills } from "../Dropdown"

export default function NotificationsOverlayLayout({
children,
close,
showAll,
showUnread,
unread,
}) {
return (
<div className="notifications-overlay-layout">
<div className="notifications-overlay-header">
<div className="notifications-overlay-caption">
{pgettext("notifications title", "Notifications")}
</div>
<button
className="btn btn-notifications-overlay"
title={pgettext("dialog", "Cancel")}
type="button"
onClick={close}
>
<span className="material-icon">close</span>
</button>
</div>
<DropdownPills>
<NotificationsOverlayLayoutPill active={!unread} onClick={showAll}>
{pgettext("notifications dropdown", "All")}
</NotificationsOverlayLayoutPill>
<NotificationsOverlayLayoutPill active={unread} onClick={showUnread}>
{pgettext("notifications dropdown", "Unread")}
</NotificationsOverlayLayoutPill>
</DropdownPills>
{children}
<DropdownFooter>
<a
className="btn btn-default btn-block"
href={misago.get("NOTIFICATIONS_URL")}
>
{pgettext("notifications", "See all notifications")}
</a>
</DropdownFooter>
</div>
)
}

function NotificationsOverlayLayoutPill({ active, children, onClick }) {
return (
<button
className={classnames("btn", {
"btn-primary": active,
"btn-default": !active,
})}
type="button"
onClick={onClick}
>
{children}
</button>
)
}
3 changes: 3 additions & 0 deletions frontend/src/components/NotificationsOverlay/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import NotificationsOverlay from "./NotificationsOverlay"

export default NotificationsOverlay
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react"
import { Dropdown } from "../Dropdown"
import NotificationsDropdown from "../NotificationsDropdown"

export default function UserNotifications({ user }) {
export default function UserNotificationsDropdown({ user }) {
let title = null
if (user.unreadNotifications) {
title = gettext("You have unread notifications!")
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/components/user-menu/UserNotificationsOverlay.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react"
import { connect } from "react-redux"
import { open } from "../../reducers/notifications"

function UserNotificationsOverlay({ dispatch, user }) {
let title = null
if (user.unreadNotifications) {
title = gettext("You have unread notifications!")
} else {
title = pgettext("navbar link", "Notifications")
}

return (
<li>
<a
className="navbar-icon"
href={misago.get("NOTIFICATIONS_URL")}
onClick={(event) => {
event.preventDefault()
dispatch(open())
}}
title={title}
>
<span className="material-icon">
{!!user.unreadNotifications
? "notifications_active"
: "notifications_none"}
</span>
{!!user.unreadNotifications && (
<span className="badge">{user.unreadNotifications}</span>
)}
</a>
</li>
)
}

const UserNotificationsOverlayConnected = connect()(UserNotificationsOverlay)

export default UserNotificationsOverlayConnected
10 changes: 6 additions & 4 deletions frontend/src/components/user-menu/user-nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import NavbarSearch from "misago/components/navbar-search"
import misago from "misago"
import dropdown from "misago/services/mobile-navbar-dropdown"
import modal from "misago/services/modal"
import UserNotifications from "./UserNotifications"
import UserNotificationsDropdown from "./UserNotificationsDropdown"
import UserNotificationsOverlay from "./UserNotificationsOverlay"

export class UserMenu extends React.Component {
changeAvatar() {
Expand Down Expand Up @@ -68,7 +69,7 @@ export class UserMenu extends React.Component {
{!!user.acl.can_use_private_threads && (
<li>
<a href={misago.get("PRIVATE_THREADS_URL")}>
<span className="material-icon">message</span>
<span className="material-icon">inbox</span>
{gettext("Private threads")}
<PrivateThreadsBadge user={user} />
</a>
Expand Down Expand Up @@ -112,7 +113,8 @@ export function UserNav({ user }) {
<NavbarSearch />
</li>
<UserPrivateThreadsLink user={user} />
<UserNotifications user={user} />
<UserNotificationsDropdown user={user} />
<UserNotificationsOverlay user={user} />
<li className="dropdown">
<a
aria-haspopup="true"
Expand Down Expand Up @@ -147,7 +149,7 @@ export function UserPrivateThreadsLink({ user }) {
href={misago.get("PRIVATE_THREADS_URL")}
title={title}
>
<span className="material-icon">message</span>
<span className="material-icon">inbox</span>
{user.unread_private_threads > 0 && (
<span className="badge">{user.unread_private_threads}</span>
)}
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/initializers/components/notifications-overlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react"
import ReactDOM from "react-dom"
import { Provider } from "react-redux"
import NotificationsOverlay from "../../components/NotificationsOverlay"
import store from "../../services/store"

export default function initializer(context) {
if (context.get("isAuthenticated")) {
const root = document.getElementById("notifications-mount")
ReactDOM.render(
<Provider store={store.getStore()}>
<NotificationsOverlay />
</Provider>,
root
)
}
}

misago.addInitializer({
name: "component:notifications-overlay",
initializer: initializer,
after: "store",
})
13 changes: 13 additions & 0 deletions frontend/src/initializers/reducers/notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import misago from "misago/index"
import reducer, { initialState } from "../../reducers/notifications"
import store from "../../services/store"

export default function initializer(context) {
store.addReducer("notifications", reducer, initialState)
}

misago.addInitializer({
name: "reducer:notifications",
initializer: initializer,
before: "store",
})
Loading

0 comments on commit 558d572

Please sign in to comment.