diff --git a/packages/pl-fe/src/columns/notifications.tsx b/packages/pl-fe/src/columns/notifications.tsx
new file mode 100644
index 000000000..666edcc0f
--- /dev/null
+++ b/packages/pl-fe/src/columns/notifications.tsx
@@ -0,0 +1,289 @@
+import clsx from 'clsx';
+import debounce from 'lodash/debounce';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+import { createSelector } from 'reselect';
+
+import {
+ type FilterType,
+ expandNotifications,
+ markReadNotifications,
+ scrollTopNotifications,
+ setFilter,
+} from 'pl-fe/actions/notifications';
+import PullToRefresh from 'pl-fe/components/pull-to-refresh';
+import ScrollableList from 'pl-fe/components/scrollable-list';
+import Icon from 'pl-fe/components/ui/icon';
+import Tabs from 'pl-fe/components/ui/tabs';
+import Notification from 'pl-fe/features/notifications/components/notification';
+import PlaceholderNotification from 'pl-fe/features/placeholder/components/placeholder-notification';
+import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
+import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
+import { useFeatures } from 'pl-fe/hooks/use-features';
+import { useSettings } from 'pl-fe/hooks/use-settings';
+
+import type { Item } from 'pl-fe/components/ui/tabs';
+import type { RootState } from 'pl-fe/store';
+import type { VirtuosoHandle } from 'react-virtuoso';
+
+const messages = defineMessages({
+ title: { id: 'column.notifications', defaultMessage: 'Notifications' },
+ queue: { id: 'notifications.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {notification} other {notifications}}' },
+ all: { id: 'notifications.filter.all', defaultMessage: 'All' },
+ mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
+ statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
+ favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Likes' },
+ boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Reposts' },
+ polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
+ events: { id: 'notifications.filter.events', defaultMessage: 'Events' },
+ follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
+});
+
+const FilterBar = () => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+ const settings = useSettings();
+ const features = useFeatures();
+
+ const selectedFilter = settings.notifications.quickFilter.active;
+ const advancedMode = settings.notifications.quickFilter.advanced;
+
+ const onClick = (notificationType: FilterType) => () => {
+ try {
+ dispatch(setFilter(notificationType, true));
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ const items: Item[] = [
+ {
+ text: intl.formatMessage(messages.all),
+ action: onClick('all'),
+ name: 'all',
+ },
+ ];
+
+ if (!advancedMode) {
+ items.push({
+ text: intl.formatMessage(messages.mentions),
+ action: onClick('mention'),
+ name: 'mention',
+ });
+ } else {
+ items.push({
+ text: ,
+ title: intl.formatMessage(messages.mentions),
+ action: onClick('mention'),
+ name: 'mention',
+ });
+ if (features.accountNotifies) items.push({
+ text: ,
+ title: intl.formatMessage(messages.statuses),
+ action: onClick('status'),
+ name: 'status',
+ });
+ items.push({
+ text: ,
+ title: intl.formatMessage(messages.favourites),
+ action: onClick('favourite'),
+ name: 'favourite',
+ });
+ items.push({
+ text: ,
+ title: intl.formatMessage(messages.boosts),
+ action: onClick('reblog'),
+ name: 'reblog',
+ });
+ if (features.polls) items.push({
+ text: ,
+ title: intl.formatMessage(messages.polls),
+ action: onClick('poll'),
+ name: 'poll',
+ });
+ if (features.events) items.push({
+ text: ,
+ title: intl.formatMessage(messages.events),
+ action: onClick('events'),
+ name: 'events',
+ });
+ items.push({
+ text: ,
+ title: intl.formatMessage(messages.follows),
+ action: onClick('follow'),
+ name: 'follow',
+ });
+ }
+
+ return ;
+};
+
+const getNotifications = createSelector([
+ (state: RootState) => state.notifications.items,
+ (_, topNotification?: string) => topNotification,
+], (notifications, topNotificationId) => {
+ if (topNotificationId) {
+ const queuedNotificationCount = notifications.findIndex((notification) =>
+ notification.most_recent_notification_id <= topNotificationId,
+ );
+ const displayedNotifications = notifications.slice(queuedNotificationCount);
+
+ return {
+ queuedNotificationCount,
+ displayedNotifications,
+ };
+ }
+
+ return {
+ queuedNotificationCount: 0,
+ displayedNotifications: notifications,
+ };
+});
+
+const NotificationsColumn = () => {
+ const dispatch = useAppDispatch();
+ const features = useFeatures();
+ const settings = useSettings();
+
+ const showFilterBar = (features.notificationsExcludeTypes || features.notificationsIncludeTypes) && settings.notifications.quickFilter.show;
+ const activeFilter = settings.notifications.quickFilter.active;
+ const [topNotification, setTopNotification] = useState();
+ const { displayedNotifications } = useAppSelector(state => getNotifications(state, topNotification));
+ const isLoading = useAppSelector(state => state.notifications.isLoading);
+ // const isUnread = useAppSelector(state => state.notifications.unread > 0);
+ const hasMore = useAppSelector(state => state.notifications.hasMore);
+
+ const node = useRef(null);
+ const scrollableContentRef = useRef | null>(null);
+
+ // const handleLoadGap = (maxId) => {
+ // dispatch(expandNotifications({ maxId }));
+ // };
+
+ const handleLoadOlder = useCallback(debounce(() => {
+ const minId = displayedNotifications.reduce(
+ (minId, notification) => minId && notification.page_min_id && notification.page_min_id > minId
+ ? minId
+ : notification.page_min_id,
+ undefined,
+ );
+ dispatch(expandNotifications({ maxId: minId }));
+ }, 300, { leading: true }), [displayedNotifications]);
+
+ const handleScrollToTop = useCallback(debounce(() => {
+ dispatch(scrollTopNotifications(true));
+ }, 100), []);
+
+ const handleScroll = useCallback(debounce(() => {
+ dispatch(scrollTopNotifications(false));
+ }, 100), []);
+
+ const handleMoveUp = (id: string) => {
+ const elementIndex = displayedNotifications.findIndex(item => item !== null && item.group_key === id) - 1;
+ _selectChild(elementIndex);
+ };
+
+ const handleMoveDown = (id: string) => {
+ const elementIndex = displayedNotifications.findIndex(item => item !== null && item.group_key === id) + 1;
+ _selectChild(elementIndex);
+ };
+
+ const _selectChild = (index: number) => {
+ const selector = `[data-index="${index}"] .focusable`;
+ const element = document.querySelector(selector);
+
+ if (element) element.focus();
+
+ node.current?.scrollIntoView({
+ index,
+ behavior: 'smooth',
+ done: () => {
+ if (!element) document.querySelector(selector)?.focus();
+ },
+ });
+ };
+
+ const handleDequeueNotifications = useCallback(() => {
+ setTopNotification(undefined);
+ dispatch(markReadNotifications());
+ }, []);
+
+ const handleRefresh = useCallback(() => dispatch(expandNotifications()), []);
+
+ useEffect(() => {
+ handleDequeueNotifications();
+ dispatch(scrollTopNotifications(true));
+
+ return () => {
+ handleLoadOlder.cancel?.();
+ handleScrollToTop.cancel();
+ handleScroll.cancel?.();
+ dispatch(scrollTopNotifications(false));
+ };
+ }, []);
+
+ useEffect(() => {
+ if (topNotification || displayedNotifications.length === 0) return;
+ setTopNotification(displayedNotifications[0].most_recent_notification_id);
+ }, [displayedNotifications.length]);
+
+ const emptyMessage = activeFilter === 'all'
+ ?
+ : ;
+
+ let scrollableContent: Array | null = null;
+
+ const filterBarContainer = showFilterBar
+ ? ()
+ : null;
+
+ if (isLoading && scrollableContentRef.current) {
+ scrollableContent = scrollableContentRef.current;
+ } else if (displayedNotifications.length > 0 || hasMore) {
+ scrollableContent = displayedNotifications.map((item) => (
+
+ ));
+ } else {
+ scrollableContent = null;
+ }
+
+ scrollableContentRef.current = scrollableContent;
+
+ const scrollContainer = (
+
+ {scrollableContent!}
+
+ );
+
+ return (
+ <>
+ {filterBarContainer}
+
+
+ {scrollContainer}
+
+ >
+ );
+};
+
+export { NotificationsColumn as default };
diff --git a/packages/pl-fe/src/pages/notifications/notifications.tsx b/packages/pl-fe/src/pages/notifications/notifications.tsx
index 2bffd338b..eeaa81405 100644
--- a/packages/pl-fe/src/pages/notifications/notifications.tsx
+++ b/packages/pl-fe/src/pages/notifications/notifications.tsx
@@ -1,126 +1,24 @@
-import clsx from 'clsx';
-import debounce from 'lodash/debounce';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+import React, { useCallback, useEffect, useState } from 'react';
+import { defineMessages, useIntl } from 'react-intl';
import { createSelector } from 'reselect';
-import {
- type FilterType,
- expandNotifications,
- markReadNotifications,
- scrollTopNotifications,
- setFilter,
-} from 'pl-fe/actions/notifications';
-import PullToRefresh from 'pl-fe/components/pull-to-refresh';
+import { markReadNotifications } from 'pl-fe/actions/notifications';
+import NotificationsColumn from 'pl-fe/columns/notifications';
import ScrollTopButton from 'pl-fe/components/scroll-top-button';
-import ScrollableList from 'pl-fe/components/scrollable-list';
import Column from 'pl-fe/components/ui/column';
-import Icon from 'pl-fe/components/ui/icon';
import Portal from 'pl-fe/components/ui/portal';
-import Tabs from 'pl-fe/components/ui/tabs';
-import Notification from 'pl-fe/features/notifications/components/notification';
-import PlaceholderNotification from 'pl-fe/features/placeholder/components/placeholder-notification';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useSettings } from 'pl-fe/hooks/use-settings';
-import type { Item } from 'pl-fe/components/ui/tabs';
import type { RootState } from 'pl-fe/store';
-import type { VirtuosoHandle } from 'react-virtuoso';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
queue: { id: 'notifications.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {notification} other {notifications}}' },
- all: { id: 'notifications.filter.all', defaultMessage: 'All' },
- mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
- statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
- favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Likes' },
- boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Reposts' },
- polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
- events: { id: 'notifications.filter.events', defaultMessage: 'Events' },
- follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
});
-const FilterBar = () => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
- const settings = useSettings();
- const features = useFeatures();
-
- const selectedFilter = settings.notifications.quickFilter.active;
- const advancedMode = settings.notifications.quickFilter.advanced;
-
- const onClick = (notificationType: FilterType) => () => {
- try {
- dispatch(setFilter(notificationType, true));
- } catch (e) {
- console.error(e);
- }
- };
-
- const items: Item[] = [
- {
- text: intl.formatMessage(messages.all),
- action: onClick('all'),
- name: 'all',
- },
- ];
-
- if (!advancedMode) {
- items.push({
- text: intl.formatMessage(messages.mentions),
- action: onClick('mention'),
- name: 'mention',
- });
- } else {
- items.push({
- text: ,
- title: intl.formatMessage(messages.mentions),
- action: onClick('mention'),
- name: 'mention',
- });
- if (features.accountNotifies) items.push({
- text: ,
- title: intl.formatMessage(messages.statuses),
- action: onClick('status'),
- name: 'status',
- });
- items.push({
- text: ,
- title: intl.formatMessage(messages.favourites),
- action: onClick('favourite'),
- name: 'favourite',
- });
- items.push({
- text: ,
- title: intl.formatMessage(messages.boosts),
- action: onClick('reblog'),
- name: 'reblog',
- });
- if (features.polls) items.push({
- text: ,
- title: intl.formatMessage(messages.polls),
- action: onClick('poll'),
- name: 'poll',
- });
- if (features.events) items.push({
- text: ,
- title: intl.formatMessage(messages.events),
- action: onClick('events'),
- name: 'events',
- });
- items.push({
- text: ,
- title: intl.formatMessage(messages.follows),
- action: onClick('follow'),
- name: 'follow',
- });
- }
-
- return ;
-};
-
const getNotifications = createSelector([
(state: RootState) => state.notifications.items,
(_, topNotification?: string) => topNotification,
@@ -150,139 +48,21 @@ const NotificationsPage = () => {
const settings = useSettings();
const showFilterBar = (features.notificationsExcludeTypes || features.notificationsIncludeTypes) && settings.notifications.quickFilter.show;
- const activeFilter = settings.notifications.quickFilter.active;
const [topNotification, setTopNotification] = useState();
const { queuedNotificationCount, displayedNotifications } = useAppSelector(state => getNotifications(state, topNotification));
- const isLoading = useAppSelector(state => state.notifications.isLoading);
- // const isUnread = useAppSelector(state => state.notifications.unread > 0);
- const hasMore = useAppSelector(state => state.notifications.hasMore);
-
- const node = useRef(null);
- const scrollableContentRef = useRef | null>(null);
-
- // const handleLoadGap = (maxId) => {
- // dispatch(expandNotifications({ maxId }));
- // };
-
- const handleLoadOlder = useCallback(debounce(() => {
- const minId = displayedNotifications.reduce(
- (minId, notification) => minId && notification.page_min_id && notification.page_min_id > minId
- ? minId
- : notification.page_min_id,
- undefined,
- );
- dispatch(expandNotifications({ maxId: minId }));
- }, 300, { leading: true }), [displayedNotifications]);
-
- const handleScrollToTop = useCallback(debounce(() => {
- dispatch(scrollTopNotifications(true));
- }, 100), []);
-
- const handleScroll = useCallback(debounce(() => {
- dispatch(scrollTopNotifications(false));
- }, 100), []);
-
- const handleMoveUp = (id: string) => {
- const elementIndex = displayedNotifications.findIndex(item => item !== null && item.group_key === id) - 1;
- _selectChild(elementIndex);
- };
-
- const handleMoveDown = (id: string) => {
- const elementIndex = displayedNotifications.findIndex(item => item !== null && item.group_key === id) + 1;
- _selectChild(elementIndex);
- };
-
- const _selectChild = (index: number) => {
- const selector = `[data-index="${index}"] .focusable`;
- const element = document.querySelector(selector);
-
- if (element) element.focus();
-
- node.current?.scrollIntoView({
- index,
- behavior: 'smooth',
- done: () => {
- if (!element) document.querySelector(selector)?.focus();
- },
- });
- };
const handleDequeueNotifications = useCallback(() => {
setTopNotification(undefined);
dispatch(markReadNotifications());
}, []);
- const handleRefresh = useCallback(() => dispatch(expandNotifications()), []);
-
- useEffect(() => {
- handleDequeueNotifications();
- dispatch(scrollTopNotifications(true));
-
- return () => {
- handleLoadOlder.cancel?.();
- handleScrollToTop.cancel();
- handleScroll.cancel?.();
- dispatch(scrollTopNotifications(false));
- };
- }, []);
-
useEffect(() => {
if (topNotification || displayedNotifications.length === 0) return;
setTopNotification(displayedNotifications[0].most_recent_notification_id);
}, [displayedNotifications.length]);
- const emptyMessage = activeFilter === 'all'
- ?
- : ;
-
- let scrollableContent: Array | null = null;
-
- const filterBarContainer = showFilterBar
- ? ()
- : null;
-
- if (isLoading && scrollableContentRef.current) {
- scrollableContent = scrollableContentRef.current;
- } else if (displayedNotifications.length > 0 || hasMore) {
- scrollableContent = displayedNotifications.map((item) => (
-
- ));
- } else {
- scrollableContent = null;
- }
-
- scrollableContentRef.current = scrollableContent;
-
- const scrollContainer = (
-
- {scrollableContent!}
-
- );
-
return (
- {filterBarContainer}
-
{
/>
-
- {scrollContainer}
-
+
);
};