Skip to content

Commit

Permalink
[Glitch] Grouped Notifications UI
Browse files Browse the repository at this point in the history
Port f587ff6 to glitch-soc

Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
  • Loading branch information
3 people committed Jul 18, 2024
1 parent c75fe09 commit 3a8a672
Show file tree
Hide file tree
Showing 56 changed files with 3,267 additions and 117 deletions.
3 changes: 3 additions & 0 deletions app/javascript/flavours/glitch/actions/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { createAction } from '@reduxjs/toolkit';

import type { LayoutType } from '../is_mobile';

export const focusApp = createAction('APP_FOCUS');
export const unfocusApp = createAction('APP_UNFOCUS');

interface ChangeLayoutPayload {
layout: LayoutType;
}
Expand Down
14 changes: 11 additions & 3 deletions app/javascript/flavours/glitch/actions/markers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,17 @@ interface MarkerParam {
}

function getLastNotificationId(state: RootState): string | undefined {
// @ts-expect-error state.notifications is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return state.getIn(['notifications', 'lastReadId']);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const enableBeta = state.settings.getIn(
['notifications', 'groupingBeta'],
false,
) as boolean;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return enableBeta
? state.notificationGroups.lastReadId
: // @ts-expect-error state.notifications is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
state.getIn(['notifications', 'lastReadId']);
}

const buildPostMarkersParams = (state: RootState) => {
Expand Down
144 changes: 144 additions & 0 deletions app/javascript/flavours/glitch/actions/notification_groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { createAction } from '@reduxjs/toolkit';

import {
apiClearNotifications,
apiFetchNotifications,
} from 'flavours/glitch/api/notifications';
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
} from 'flavours/glitch/api_types/notifications';
import { allNotificationTypes } from 'flavours/glitch/api_types/notifications';
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
import type { NotificationGap } from 'flavours/glitch/reducers/notification_groups';
import {
selectSettingsNotificationsExcludedTypes,
selectSettingsNotificationsQuickFilterActive,
} from 'flavours/glitch/selectors/settings';
import type { AppDispatch } from 'flavours/glitch/store';
import {
createAppAsyncThunk,
createDataLoadingThunk,
} from 'flavours/glitch/store/typed_functions';

import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { NOTIFICATIONS_FILTER_SET } from './notifications';
import { saveSettings } from './settings';

function excludeAllTypesExcept(filter: string) {
return allNotificationTypes.filter((item) => item !== filter);
}

function dispatchAssociatedRecords(
dispatch: AppDispatch,
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
) {
const fetchedAccounts: ApiAccountJSON[] = [];
const fetchedStatuses: ApiStatusJSON[] = [];

notifications.forEach((notification) => {
if ('sample_accounts' in notification) {
fetchedAccounts.push(...notification.sample_accounts);
}

if (notification.type === 'admin.report') {
fetchedAccounts.push(notification.report.target_account);
}

if (notification.type === 'moderation_warning') {
fetchedAccounts.push(notification.moderation_warning.target_account);
}

if ('status' in notification) {
fetchedStatuses.push(notification.status);
}
});

if (fetchedAccounts.length > 0)
dispatch(importFetchedAccounts(fetchedAccounts));

if (fetchedStatuses.length > 0)
dispatch(importFetchedStatuses(fetchedStatuses));
}

export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch',
async (_params, { getState }) => {
const activeFilter =
selectSettingsNotificationsQuickFilterActive(getState());

return apiFetchNotifications({
exclude_types:
activeFilter === 'all'
? selectSettingsNotificationsExcludedTypes(getState())
: excludeAllTypesExcept(activeFilter),
});
},
({ notifications }, { dispatch }) => {
dispatchAssociatedRecords(dispatch, notifications);
const payload: (ApiNotificationGroupJSON | NotificationGap)[] =
notifications;

// TODO: might be worth not using gaps for that…
// if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri });
if (notifications.length > 1)
payload.push({ type: 'gap', maxId: notifications.at(-1)?.page_min_id });

return payload;
// dispatch(submitMarkers());
},
);

export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }) =>
apiFetchNotifications({ max_id: params.gap.maxId }),

({ notifications }, { dispatch }) => {
dispatchAssociatedRecords(dispatch, notifications);

return { notifications };
},
);

export const processNewNotificationForGroups = createAppAsyncThunk(
'notificationGroups/processNew',
(notification: ApiNotificationJSON, { dispatch }) => {
dispatchAssociatedRecords(dispatch, [notification]);

return notification;
},
);

export const loadPending = createAction('notificationGroups/loadPending');

export const updateScrollPosition = createAction<{ top: boolean }>(
'notificationGroups/updateScrollPosition',
);

export const setNotificationsFilter = createAppAsyncThunk(
'notifications/filter/set',
({ filterType }: { filterType: string }, { dispatch }) => {
dispatch({
type: NOTIFICATIONS_FILTER_SET,
path: ['notifications', 'quickFilter', 'active'],
value: filterType,
});
// dispatch(expandNotifications({ forceLoad: true }));
void dispatch(fetchNotifications());
dispatch(saveSettings());
},
);

export const clearNotifications = createDataLoadingThunk(
'notifications/clear',
() => apiClearNotifications(),
);

export const markNotificationsAsRead = createAction(
'notificationGroups/markAsRead',
);

export const mountNotifications = createAction('notificationGroups/mount');
export const unmountNotifications = createAction('notificationGroups/unmount');
13 changes: 1 addition & 12 deletions app/javascript/flavours/glitch/actions/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';

export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';

export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';

Expand Down Expand Up @@ -186,7 +185,7 @@ const noOp = () => {};

let expandNotificationsController = new AbortController();

export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
export function expandNotifications({ maxId, forceLoad = false } = {}, done = noOp) {
return (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
const notifications = getState().get('notifications');
Expand Down Expand Up @@ -269,16 +268,6 @@ export function expandNotificationsFail(error, isLoadingMore) {
};
}

export function clearNotifications() {
return (dispatch) => {
dispatch({
type: NOTIFICATIONS_CLEAR,
});

api().post('/api/v1/notifications/clear');
};
}

export function scrollTopNotifications(top) {
return {
type: NOTIFICATIONS_SCROLL_TOP,
Expand Down
18 changes: 18 additions & 0 deletions app/javascript/flavours/glitch/actions/notifications_migration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createAppAsyncThunk } from 'flavours/glitch/store';

import { fetchNotifications } from './notification_groups';
import { expandNotifications } from './notifications';

export const initializeNotifications = createAppAsyncThunk(
'notifications/initialize',
(_, { dispatch, getState }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const enableBeta = getState().settings.getIn(
['notifications', 'groupingBeta'],
false,
) as boolean;

if (enableBeta) void dispatch(fetchNotifications());
else dispatch(expandNotifications());
},
);
9 changes: 2 additions & 7 deletions app/javascript/flavours/glitch/actions/notifications_typed.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { createAction } from '@reduxjs/toolkit';

import type { ApiAccountJSON } from '../api_types/accounts';
// To be replaced once ApiNotificationJSON type exists
interface FakeApiNotificationJSON {
type: string;
account: ApiAccountJSON;
}
import type { ApiNotificationJSON } from 'flavours/glitch/api_types/notifications';

export const notificationsUpdate = createAction(
'notifications/update',
({
playSound,
...args
}: {
notification: FakeApiNotificationJSON;
notification: ApiNotificationJSON;
usePendingItems: boolean;
playSound: boolean;
}) => ({
Expand Down
11 changes: 9 additions & 2 deletions app/javascript/flavours/glitch/actions/streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
deleteAnnouncement,
} from './announcements';
import { updateConversations } from './conversations';
import { processNewNotificationForGroups } from './notification_groups';
import { updateNotifications, expandNotifications } from './notifications';
import { updateStatus } from './statuses';
import {
Expand Down Expand Up @@ -98,10 +99,16 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
case 'notification': {
// @ts-expect-error
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
const notificationJSON = JSON.parse(data.payload);
dispatch(updateNotifications(notificationJSON, messages, locale));
// TODO: remove this once the groups feature replaces the previous one
if(getState().notificationGroups.groups.length > 0) {
dispatch(processNewNotificationForGroups(notificationJSON));
}
break;
}
case 'conversation':
// @ts-expect-error
dispatch(updateConversations(JSON.parse(data.payload)));
Expand Down
18 changes: 18 additions & 0 deletions app/javascript/flavours/glitch/api/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import api, { apiRequest, getLinks } from 'flavours/glitch/api';
import type { ApiNotificationGroupJSON } from 'flavours/glitch/api_types/notifications';

export const apiFetchNotifications = async (params?: {
exclude_types?: string[];
max_id?: string;
}) => {
const response = await api().request<ApiNotificationGroupJSON[]>({
method: 'GET',
url: '/api/v2_alpha/notifications',
params,
});

return { notifications: response.data, links: getLinks(response) };
};

export const apiClearNotifications = () =>
apiRequest<undefined>('POST', 'v1/notifications/clear');
Loading

0 comments on commit 3a8a672

Please sign in to comment.