sforkowany z mirror/soapbox
Porównaj commity
8 Commity
develop
...
entity-sto
Autor | SHA1 | Data |
---|---|---|
Alex Gleason | 92b7eb0ffe | |
Alex Gleason | bd64fc4304 | |
Alex Gleason | e8d7517231 | |
Alex Gleason | babd66b25c | |
Alex Gleason | 27500193d8 | |
Alex Gleason | f7bfc40b70 | |
Alex Gleason | 52059f6f37 | |
Alex Gleason | 3b067c6fab |
|
@ -29,6 +29,10 @@ export const getNextLink = (response: AxiosResponse): string | undefined => {
|
|||
return getLinks(response).refs.find(link => link.rel === 'next')?.uri;
|
||||
};
|
||||
|
||||
export const getPrevLink = (response: AxiosResponse): string | undefined => {
|
||||
return getLinks(response).refs.find(link => link.rel === 'prev')?.uri;
|
||||
};
|
||||
|
||||
const getToken = (state: RootState, authType: string) => {
|
||||
return authType === 'app' ? getAppToken(state) : getAccessToken(state);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import type { Entity, EntityListState } from './types';
|
||||
|
||||
const ENTITIES_IMPORT = 'ENTITIES_IMPORT' as const;
|
||||
const ENTITIES_FETCH_REQUEST = 'ENTITIES_FETCH_REQUEST' as const;
|
||||
const ENTITIES_FETCH_SUCCESS = 'ENTITIES_FETCH_SUCCESS' as const;
|
||||
const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const;
|
||||
|
||||
/** Action to import entities into the cache. */
|
||||
function importEntities(entities: Entity[], entityType: string, listKey?: string) {
|
||||
return {
|
||||
type: ENTITIES_IMPORT,
|
||||
entityType,
|
||||
entities,
|
||||
listKey,
|
||||
};
|
||||
}
|
||||
|
||||
function entitiesFetchRequest(entityType: string, listKey?: string) {
|
||||
return {
|
||||
type: ENTITIES_FETCH_REQUEST,
|
||||
entityType,
|
||||
listKey,
|
||||
};
|
||||
}
|
||||
|
||||
function entitiesFetchSuccess(entities: Entity[], entityType: string, listKey?: string, newState?: EntityListState) {
|
||||
return {
|
||||
type: ENTITIES_FETCH_SUCCESS,
|
||||
entityType,
|
||||
entities,
|
||||
listKey,
|
||||
newState,
|
||||
};
|
||||
}
|
||||
|
||||
function entitiesFetchFail(entityType: string, listKey: string | undefined, error: any) {
|
||||
return {
|
||||
type: ENTITIES_FETCH_FAIL,
|
||||
entityType,
|
||||
listKey,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
/** Any action pertaining to entities. */
|
||||
type EntityAction =
|
||||
ReturnType<typeof importEntities>
|
||||
| ReturnType<typeof entitiesFetchRequest>
|
||||
| ReturnType<typeof entitiesFetchSuccess>
|
||||
| ReturnType<typeof entitiesFetchFail>;
|
||||
|
||||
export {
|
||||
ENTITIES_IMPORT,
|
||||
ENTITIES_FETCH_REQUEST,
|
||||
ENTITIES_FETCH_SUCCESS,
|
||||
ENTITIES_FETCH_FAIL,
|
||||
importEntities,
|
||||
entitiesFetchRequest,
|
||||
entitiesFetchSuccess,
|
||||
entitiesFetchFail,
|
||||
EntityAction,
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export { useEntities } from './useEntities';
|
||||
export { useEntity } from './useEntity';
|
|
@ -0,0 +1,85 @@
|
|||
import { getNextLink, getPrevLink } from 'soapbox/api';
|
||||
import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions';
|
||||
|
||||
import type { Entity } from '../types';
|
||||
|
||||
type EntityPath = [entityType: string, listKey: string]
|
||||
|
||||
function useEntities<TEntity extends Entity>(path: EntityPath, endpoint: string) {
|
||||
const api = useApi();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [entityType, listKey] = path;
|
||||
|
||||
const cache = useAppSelector(state => state.entities[entityType]);
|
||||
const list = cache?.lists[listKey];
|
||||
|
||||
const entityIds = list?.ids;
|
||||
|
||||
const entities: readonly TEntity[] = entityIds ? (
|
||||
Array.from(entityIds).reduce<TEntity[]>((result, id) => {
|
||||
const entity = cache?.store[id] as TEntity | undefined;
|
||||
if (entity) {
|
||||
result.push(entity);
|
||||
}
|
||||
return result;
|
||||
}, [])
|
||||
) : [];
|
||||
|
||||
const isFetching = Boolean(list?.state.fetching);
|
||||
const isLoading = isFetching && entities.length === 0;
|
||||
const hasNextPage = Boolean(list?.state.next);
|
||||
const hasPreviousPage = Boolean(list?.state.prev);
|
||||
|
||||
const fetchPage = async(url: string): Promise<void> => {
|
||||
dispatch(entitiesFetchRequest(entityType, listKey));
|
||||
try {
|
||||
const response = await api.get(url);
|
||||
dispatch(entitiesFetchSuccess(response.data, entityType, listKey, {
|
||||
next: getNextLink(response),
|
||||
prev: getPrevLink(response),
|
||||
fetching: false,
|
||||
error: null,
|
||||
}));
|
||||
} catch (error) {
|
||||
dispatch(entitiesFetchFail(entityType, listKey, error));
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEntities = async(): Promise<void> => {
|
||||
await fetchPage(endpoint);
|
||||
};
|
||||
|
||||
const fetchNextPage = async(): Promise<void> => {
|
||||
const next = list?.state.next;
|
||||
|
||||
if (next) {
|
||||
await fetchPage(next);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPreviousPage = async(): Promise<void> => {
|
||||
const prev = list?.state.prev;
|
||||
|
||||
if (prev) {
|
||||
await fetchPage(prev);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
entities,
|
||||
fetchEntities,
|
||||
isFetching,
|
||||
isLoading,
|
||||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
fetchNextPage,
|
||||
fetchPreviousPage,
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
useEntities,
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import { importEntities } from '../actions';
|
||||
|
||||
import type { Entity } from '../types';
|
||||
|
||||
type EntityPath = [entityType: string, entityId: string]
|
||||
|
||||
function useEntity<TEntity extends Entity>(path: EntityPath, endpoint: string) {
|
||||
const api = useApi();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [entityType, entityId] = path;
|
||||
const entity = useAppSelector(state => state.entities[entityType]?.store[entityId]) as TEntity | undefined;
|
||||
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const isLoading = isFetching && !entity;
|
||||
|
||||
const fetchEntity = () => {
|
||||
setIsFetching(true);
|
||||
api.get(endpoint).then(({ data }) => {
|
||||
dispatch(importEntities([data], entityType));
|
||||
setIsFetching(false);
|
||||
}).catch(() => {
|
||||
setIsFetching(false);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
entity,
|
||||
fetchEntity,
|
||||
isFetching,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
useEntity,
|
||||
};
|
|
@ -0,0 +1,81 @@
|
|||
import produce, { enableMapSet } from 'immer';
|
||||
|
||||
import {
|
||||
ENTITIES_IMPORT,
|
||||
ENTITIES_FETCH_REQUEST,
|
||||
ENTITIES_FETCH_SUCCESS,
|
||||
ENTITIES_FETCH_FAIL,
|
||||
EntityAction,
|
||||
} from './actions';
|
||||
import { createCache, createList, updateStore, updateList } from './utils';
|
||||
|
||||
import type { Entity, EntityCache, EntityListState } from './types';
|
||||
|
||||
enableMapSet();
|
||||
|
||||
/** Entity reducer state. */
|
||||
interface State {
|
||||
[entityType: string]: EntityCache | undefined
|
||||
}
|
||||
|
||||
/** Import entities into the cache. */
|
||||
const importEntities = (
|
||||
state: State,
|
||||
entityType: string,
|
||||
entities: Entity[],
|
||||
listKey?: string,
|
||||
newState?: EntityListState,
|
||||
): State => {
|
||||
return produce(state, draft => {
|
||||
const cache = draft[entityType] ?? createCache();
|
||||
cache.store = updateStore(cache.store, entities);
|
||||
|
||||
if (typeof listKey === 'string') {
|
||||
let list = { ...(cache.lists[listKey] ?? createList()) };
|
||||
list = updateList(list, entities);
|
||||
if (newState) {
|
||||
list.state = newState;
|
||||
}
|
||||
cache.lists[listKey] = list;
|
||||
}
|
||||
|
||||
draft[entityType] = cache;
|
||||
});
|
||||
};
|
||||
|
||||
const setFetching = (
|
||||
state: State,
|
||||
entityType: string,
|
||||
listKey: string | undefined,
|
||||
isFetching: boolean,
|
||||
) => {
|
||||
return produce(state, draft => {
|
||||
const cache = draft[entityType] ?? createCache();
|
||||
|
||||
if (typeof listKey === 'string') {
|
||||
const list = cache.lists[listKey] ?? createList();
|
||||
list.state.fetching = isFetching;
|
||||
cache.lists[listKey] = list;
|
||||
}
|
||||
|
||||
draft[entityType] = cache;
|
||||
});
|
||||
};
|
||||
|
||||
/** Stores various entity data and lists in a one reducer. */
|
||||
function reducer(state: Readonly<State> = {}, action: EntityAction): State {
|
||||
switch (action.type) {
|
||||
case ENTITIES_IMPORT:
|
||||
return importEntities(state, action.entityType, action.entities, action.listKey);
|
||||
case ENTITIES_FETCH_SUCCESS:
|
||||
return importEntities(state, action.entityType, action.entities, action.listKey, action.newState);
|
||||
case ENTITIES_FETCH_REQUEST:
|
||||
return setFetching(state, action.entityType, action.listKey, true);
|
||||
case ENTITIES_FETCH_FAIL:
|
||||
return setFetching(state, action.entityType, action.listKey, false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,48 @@
|
|||
/** A Mastodon API entity. */
|
||||
interface Entity {
|
||||
/** Unique ID for the entity (usually the primary key in the database). */
|
||||
id: string
|
||||
}
|
||||
|
||||
/** Store of entities by ID. */
|
||||
interface EntityStore {
|
||||
[id: string]: Entity | undefined
|
||||
}
|
||||
|
||||
/** List of entity IDs and fetch state. */
|
||||
interface EntityList {
|
||||
/** Set of entity IDs in this list. */
|
||||
ids: Set<string>
|
||||
/** Server state for this entity list. */
|
||||
state: EntityListState
|
||||
}
|
||||
|
||||
/** Fetch state for an entity list. */
|
||||
interface EntityListState {
|
||||
/** Next URL for pagination, if any. */
|
||||
next: string | undefined
|
||||
/** Previous URL for pagination, if any. */
|
||||
prev: string | undefined
|
||||
/** Error returned from the API, if any. */
|
||||
error: any
|
||||
/** Whether data for this list is currently being fetched. */
|
||||
fetching: boolean
|
||||
}
|
||||
|
||||
/** Cache data pertaining to a paritcular entity type.. */
|
||||
interface EntityCache {
|
||||
/** Map of entities of this type. */
|
||||
store: EntityStore
|
||||
/** Lists of entity IDs for a particular purpose. */
|
||||
lists: {
|
||||
[listKey: string]: EntityList | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Entity,
|
||||
EntityStore,
|
||||
EntityList,
|
||||
EntityListState,
|
||||
EntityCache,
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import type { Entity, EntityStore, EntityList, EntityCache } from './types';
|
||||
|
||||
/** Insert the entities into the store. */
|
||||
const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => {
|
||||
return entities.reduce<EntityStore>((store, entity) => {
|
||||
store[entity.id] = entity;
|
||||
return store;
|
||||
}, { ...store });
|
||||
};
|
||||
|
||||
/** Update the list with new entity IDs. */
|
||||
const updateList = (list: EntityList, entities: Entity[]): EntityList => {
|
||||
const newIds = entities.map(entity => entity.id);
|
||||
return {
|
||||
...list,
|
||||
ids: new Set([...Array.from(list.ids), ...newIds]),
|
||||
};
|
||||
};
|
||||
|
||||
/** Create an empty entity cache. */
|
||||
const createCache = (): EntityCache => ({
|
||||
store: {},
|
||||
lists: {},
|
||||
});
|
||||
|
||||
/** Create an empty entity list. */
|
||||
const createList = (): EntityList => ({
|
||||
ids: new Set(),
|
||||
state: {
|
||||
next: undefined,
|
||||
prev: undefined,
|
||||
fetching: false,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
|
||||
export {
|
||||
updateStore,
|
||||
updateList,
|
||||
createCache,
|
||||
createList,
|
||||
};
|
|
@ -9,9 +9,9 @@ import { openModal } from 'soapbox/actions/modals';
|
|||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { hideStatus, revealStatus } from 'soapbox/actions/statuses';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import Status from 'soapbox/components/status';
|
||||
import { HStack, Text, Emoji } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import StatusContainer from 'soapbox/containers/status-container';
|
||||
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
|
||||
import { makeGetNotification } from 'soapbox/selectors';
|
||||
import { NotificationType, validType } from 'soapbox/utils/notification';
|
||||
|
@ -162,14 +162,9 @@ interface INotificaton {
|
|||
}
|
||||
|
||||
const Notification: React.FC<INotificaton> = (props) => {
|
||||
const { hidden = false, onMoveUp, onMoveDown } = props;
|
||||
const { notification, hidden = false, onMoveUp, onMoveDown } = props;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getNotification = useCallback(makeGetNotification(), []);
|
||||
|
||||
const notification = useAppSelector((state) => getNotification(state, props.notification));
|
||||
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
const instance = useInstance();
|
||||
|
@ -321,8 +316,8 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
case 'pleroma:participation_accepted':
|
||||
case 'pleroma:participation_request':
|
||||
return status && typeof status === 'object' ? (
|
||||
<StatusContainer
|
||||
id={status.id}
|
||||
<Status
|
||||
status={status}
|
||||
withDismiss
|
||||
hidden={hidden}
|
||||
onMoveDown={handleMoveDown}
|
||||
|
|
|
@ -17,6 +17,7 @@ import ScrollableList from 'soapbox/components/scrollable-list';
|
|||
import { Column } from 'soapbox/components/ui';
|
||||
import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder-notification';
|
||||
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
|
||||
import { useNotifications } from 'soapbox/hooks/useNotifications';
|
||||
|
||||
import FilterBar from './components/filter-bar';
|
||||
import Notification from './components/notification';
|
||||
|
@ -50,25 +51,25 @@ const Notifications = () => {
|
|||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
|
||||
const {
|
||||
entities: notifications,
|
||||
isLoading,
|
||||
isFetching,
|
||||
hasNextPage: hasMore,
|
||||
fetchEntities,
|
||||
fetchNextPage,
|
||||
} = useNotifications();
|
||||
|
||||
const showFilterBar = settings.getIn(['notifications', 'quickFilter', 'show']);
|
||||
const activeFilter = settings.getIn(['notifications', 'quickFilter', 'active']);
|
||||
const notifications = useAppSelector(state => getNotifications(state));
|
||||
const isLoading = useAppSelector(state => state.notifications.isLoading);
|
||||
// const isUnread = useAppSelector(state => state.notifications.unread > 0);
|
||||
const hasMore = useAppSelector(state => state.notifications.hasMore);
|
||||
const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0);
|
||||
|
||||
const node = useRef<VirtuosoHandle>(null);
|
||||
const column = useRef<HTMLDivElement>(null);
|
||||
const scrollableContentRef = useRef<ImmutableList<JSX.Element> | null>(null);
|
||||
|
||||
// const handleLoadGap = (maxId) => {
|
||||
// dispatch(expandNotifications({ maxId }));
|
||||
// };
|
||||
const scrollableContentRef = useRef<Iterable<JSX.Element> | null>(null);
|
||||
|
||||
const handleLoadOlder = useCallback(debounce(() => {
|
||||
const last = notifications.last();
|
||||
dispatch(expandNotifications({ maxId: last && last.get('id') }));
|
||||
fetchNextPage();
|
||||
}, 300, { leading: true }), [notifications]);
|
||||
|
||||
const handleScrollToTop = useCallback(debounce(() => {
|
||||
|
@ -112,6 +113,12 @@ const Notifications = () => {
|
|||
return dispatch(expandNotifications());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetching) {
|
||||
fetchEntities();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleDequeueNotifications();
|
||||
dispatch(scrollTopNotifications(true));
|
||||
|
@ -128,7 +135,7 @@ const Notifications = () => {
|
|||
? <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />
|
||||
: <FormattedMessage id='empty_column.notifications_filtered' defaultMessage="You don't have any notifications of this type yet." />;
|
||||
|
||||
let scrollableContent: ImmutableList<JSX.Element> | null = null;
|
||||
let scrollableContent: Iterable<JSX.Element> | null = null;
|
||||
|
||||
const filterBarContainer = showFilterBar
|
||||
? (<FilterBar />)
|
||||
|
@ -136,7 +143,7 @@ const Notifications = () => {
|
|||
|
||||
if (isLoading && scrollableContentRef.current) {
|
||||
scrollableContent = scrollableContentRef.current;
|
||||
} else if (notifications.size > 0 || hasMore) {
|
||||
} else if (notifications.length > 0 || hasMore) {
|
||||
scrollableContent = notifications.map((item) => (
|
||||
<Notification
|
||||
key={item.id}
|
||||
|
@ -156,7 +163,7 @@ const Notifications = () => {
|
|||
ref={node}
|
||||
scrollKey='notifications'
|
||||
isLoading={isLoading}
|
||||
showLoading={isLoading && notifications.size === 0}
|
||||
showLoading={isLoading && notifications.length === 0}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={emptyMessage}
|
||||
placeholderComponent={PlaceholderNotification}
|
||||
|
@ -165,8 +172,8 @@ const Notifications = () => {
|
|||
onScrollToTop={handleScrollToTop}
|
||||
onScroll={handleScroll}
|
||||
className={classNames({
|
||||
'divide-y divide-gray-200 dark:divide-primary-800 divide-solid': notifications.size > 0,
|
||||
'space-y-2': notifications.size === 0,
|
||||
'divide-y divide-gray-200 dark:divide-primary-800 divide-solid': notifications.length > 0,
|
||||
'space-y-2': notifications.length === 0,
|
||||
})}
|
||||
>
|
||||
{scrollableContent as ImmutableList<JSX.Element>}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { normalizeNotification } from 'soapbox/normalizers';
|
||||
|
||||
import type { Notification } from 'soapbox/types/entities';
|
||||
|
||||
function useNotifications() {
|
||||
const result = useEntities<Notification>(['Notification', ''], '/api/v1/notifications');
|
||||
|
||||
return {
|
||||
...result,
|
||||
// TODO: handle this in the reducer by passing config.
|
||||
entities: result.entities.map(normalizeNotification),
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
useNotifications,
|
||||
};
|
|
@ -9,6 +9,9 @@ import {
|
|||
fromJS,
|
||||
} from 'immutable';
|
||||
|
||||
import { normalizeAccount } from './account';
|
||||
import { normalizeStatus } from './status';
|
||||
|
||||
import type { Account, Status, EmbeddedEntity } from 'soapbox/types/entities';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/notification/
|
||||
|
@ -26,6 +29,14 @@ export const NotificationRecord = ImmutableRecord({
|
|||
|
||||
export const normalizeNotification = (notification: Record<string, any>) => {
|
||||
return NotificationRecord(
|
||||
ImmutableMap(fromJS(notification)),
|
||||
ImmutableMap(fromJS(notification)).withMutations(notification => {
|
||||
if (notification.get('status')) {
|
||||
notification.set('status', normalizeStatus(notification.get('status') as any) as any);
|
||||
}
|
||||
|
||||
if (notification.get('account')) {
|
||||
notification.set('account', normalizeAccount(notification.get('account') as any) as any);
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* Converts API statuses into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/status/}
|
||||
*/
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import {
|
||||
Map as ImmutableMap,
|
||||
List as ImmutableList,
|
||||
|
@ -10,17 +11,26 @@ import {
|
|||
fromJS,
|
||||
} from 'immutable';
|
||||
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import { normalizeAccount } from 'soapbox/normalizers/account';
|
||||
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
||||
import { normalizeCard } from 'soapbox/normalizers/card';
|
||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||
import { normalizeMention } from 'soapbox/normalizers/mention';
|
||||
import { normalizePoll } from 'soapbox/normalizers/poll';
|
||||
import { stripCompatibilityFeatures } from 'soapbox/utils/html';
|
||||
import { buildSearchContent, makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||
|
||||
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||
import type { Account, Attachment, Card, Emoji, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
|
||||
|
||||
export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'self';
|
||||
|
||||
/** Maximum number of times to recurse sub-entities. */
|
||||
const MAX_DEPTH = 1;
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
export type EventJoinMode = 'free' | 'restricted' | 'invite';
|
||||
export type EventJoinState = 'pending' | 'reject' | 'accept';
|
||||
|
||||
|
@ -83,6 +93,11 @@ export const StatusRecord = ImmutableRecord({
|
|||
translation: null as ImmutableMap<string, string> | null,
|
||||
});
|
||||
|
||||
const normalizeStatusAccount = (status: ImmutableMap<string, any>) => {
|
||||
const account = normalizeAccount(status.get('account'));
|
||||
return status.set('account', account);
|
||||
};
|
||||
|
||||
const normalizeAttachments = (status: ImmutableMap<string, any>) => {
|
||||
return status.update('media_attachments', ImmutableList(), attachments => {
|
||||
return attachments.map(normalizeAttachment);
|
||||
|
@ -176,6 +191,40 @@ const fixSensitivity = (status: ImmutableMap<string, any>) => {
|
|||
}
|
||||
};
|
||||
|
||||
const normalizeReblogQuote = (status: ImmutableMap<string, any>, depth = 0) => {
|
||||
if (depth >= MAX_DEPTH) {
|
||||
return status;
|
||||
} else {
|
||||
depth++;
|
||||
}
|
||||
|
||||
const reblog = status.get('reblog');
|
||||
const quote = status.get('quote');
|
||||
|
||||
return status.withMutations(status => {
|
||||
if (reblog) {
|
||||
status.set('reblog', normalizeStatus(reblog, depth));
|
||||
}
|
||||
|
||||
if (quote) {
|
||||
status.set('quote', normalizeStatus(quote, depth));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addInternalFields = (status: ImmutableMap<string, any>) => {
|
||||
const spoilerText = status.get('spoiler_text');
|
||||
const searchContent = buildSearchContent(status);
|
||||
const emojiMap = makeEmojiMap(status.get('emojis'));
|
||||
|
||||
return status.merge({
|
||||
search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '',
|
||||
contentHtml: stripCompatibilityFeatures(emojify(status.get('content'), emojiMap)),
|
||||
spoilerHtml: emojify(escapeTextContentForBrowser(spoilerText), emojiMap),
|
||||
// hidden: expandSpoilers ? false : spoilerText.length > 0 || status.get('sensitive'),
|
||||
});
|
||||
};
|
||||
|
||||
// Normalize event
|
||||
const normalizeEvent = (status: ImmutableMap<string, any>) => {
|
||||
if (status.getIn(['pleroma', 'event'])) {
|
||||
|
@ -203,9 +252,10 @@ const normalizeEvent = (status: ImmutableMap<string, any>) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const normalizeStatus = (status: Record<string, any>) => {
|
||||
export const normalizeStatus = (status: Record<string, any>, depth = 0) => {
|
||||
return StatusRecord(
|
||||
ImmutableMap(fromJS(status)).withMutations(status => {
|
||||
normalizeStatusAccount(status);
|
||||
normalizeAttachments(status);
|
||||
normalizeMentions(status);
|
||||
normalizeEmojis(status);
|
||||
|
@ -216,6 +266,8 @@ export const normalizeStatus = (status: Record<string, any>) => {
|
|||
fixQuote(status);
|
||||
fixFiltered(status);
|
||||
fixSensitivity(status);
|
||||
normalizeReblogQuote(status, depth);
|
||||
addInternalFields(status);
|
||||
normalizeEvent(status);
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ import { combineReducers } from 'redux-immutable';
|
|||
|
||||
import { AUTH_LOGGED_OUT } from 'soapbox/actions/auth';
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import entities from 'soapbox/entity-store/reducer';
|
||||
|
||||
import account_notes from './account-notes';
|
||||
import accounts from './accounts';
|
||||
|
@ -120,6 +121,7 @@ const reducers = {
|
|||
history,
|
||||
announcements,
|
||||
compose_event,
|
||||
entities,
|
||||
};
|
||||
|
||||
// Build a default state from all reducers: it has the key and `undefined`
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import { unescapeHTML } from './html';
|
||||
|
||||
/** Use new value only if old value is undefined */
|
||||
export const mergeDefined = (oldVal: any, newVal: any) => oldVal === undefined ? newVal : oldVal;
|
||||
|
||||
|
@ -10,3 +14,32 @@ export const makeEmojiMap = (emojis: any) => emojis.reduce((obj: any, emoji: any
|
|||
export const normalizeId = (id: any): string | null => {
|
||||
return typeof id === 'string' ? id : null;
|
||||
};
|
||||
|
||||
/** Gets titles of poll options from status. */
|
||||
const getPollOptionTitles = (status: ImmutableMap<string, any>): ImmutableList<string> => {
|
||||
const poll = status.get('poll');
|
||||
|
||||
if (poll && typeof poll === 'object') {
|
||||
return poll.get('options').map((option: ImmutableMap<string, any>) => option.get('title'));
|
||||
} else {
|
||||
return ImmutableList();
|
||||
}
|
||||
};
|
||||
|
||||
/** Gets usernames of mentioned users from status. */
|
||||
const getMentionedUsernames = (status: ImmutableMap<string, any>): ImmutableList<string> => {
|
||||
return status.get('mentions').map((mention: ImmutableMap<string, any>) => `@${mention.get('acct')}`);
|
||||
};
|
||||
|
||||
/** Creates search text from the status. */
|
||||
export const buildSearchContent = (status: ImmutableMap<string, any>): string => {
|
||||
const pollOptionTitles = getPollOptionTitles(status);
|
||||
const mentionedUsernames = getMentionedUsernames(status);
|
||||
|
||||
const fields = ImmutableList([
|
||||
status.get('spoiler_text'),
|
||||
status.get('content'),
|
||||
]).concat(pollOptionTitles).concat(mentionedUsernames);
|
||||
|
||||
return unescapeHTML(fields.join('\n\n')) || '';
|
||||
};
|
|
@ -129,6 +129,7 @@
|
|||
"html-webpack-harddisk-plugin": "^2.0.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"http-link-header": "^1.0.2",
|
||||
"immer": "^9.0.16",
|
||||
"immutable": "^4.0.0",
|
||||
"imports-loader": "^4.0.0",
|
||||
"intersection-observer": "^0.12.0",
|
||||
|
|
|
@ -6707,6 +6707,11 @@ immediate@~3.0.5:
|
|||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
|
||||
|
||||
immer@^9.0.16:
|
||||
version "9.0.16"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.16.tgz#8e7caab80118c2b54b37ad43e05758cdefad0198"
|
||||
integrity sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==
|
||||
|
||||
immer@^9.0.7:
|
||||
version "9.0.12"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20"
|
||||
|
|
Ładowanie…
Reference in New Issue