Convert selectors/index to Typescript

This commit is contained in:
Alex Gleason 2022-03-31 17:00:31 -05:00
parent 830fb67215
commit dddba516fb
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
12 changed files with 456 additions and 404 deletions

View File

@ -13,7 +13,6 @@ import {
import emojify from 'soapbox/features/emoji/emoji'; import emojify from 'soapbox/features/emoji/emoji';
import { normalizeEmoji } from 'soapbox/normalizers/emoji'; import { normalizeEmoji } from 'soapbox/normalizers/emoji';
import { acctFull } from 'soapbox/utils/accounts';
import { unescapeHTML } from 'soapbox/utils/html'; import { unescapeHTML } from 'soapbox/utils/html';
import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers'; import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers';
@ -197,8 +196,29 @@ const addInternalFields = (account: ImmutableMap<string, any>) => {
}); });
}; };
const getDomainFromURL = (account: ImmutableMap<string, any>): string => {
try {
const url = account.get('url');
return new URL(url).host;
} catch {
return '';
}
};
export const guessFqn = (account: ImmutableMap<string, any>): string => {
const acct = account.get('acct', '');
const [user, domain] = acct.split('@');
if (domain) {
return acct;
} else {
return [user, getDomainFromURL(account)].join('@');
}
};
const normalizeFqn = (account: ImmutableMap<string, any>) => { const normalizeFqn = (account: ImmutableMap<string, any>) => {
return account.set('fqn', acctFull(account)); const fqn = account.get('fqn') || guessFqn(account);
return account.set('fqn', fqn);
}; };
export const normalizeAccount = (account: Record<string, any>) => { export const normalizeAccount = (account: Record<string, any>) => {

View File

@ -26,8 +26,8 @@ export const AttachmentRecord = ImmutableRecord({
// Internal fields // Internal fields
// TODO: Remove these? They're set in selectors/index.js // TODO: Remove these? They're set in selectors/index.js
account: null, account: null as any,
status: null, status: null as any,
}); });
// Ensure attachments have required fields // Ensure attachments have required fields

View File

@ -9,16 +9,30 @@ import {
fromJS, fromJS,
} from 'immutable'; } from 'immutable';
import type { Account, Status, EmbeddedEntity } from 'soapbox/types/entities';
type NotificationType = ''
| 'follow'
| 'follow_request'
| 'mention'
| 'reblog'
| 'favourite'
| 'poll'
| 'status'
| 'move'
| 'pleroma:chat_mention'
| 'pleroma:emoji_reaction';
// https://docs.joinmastodon.org/entities/notification/ // https://docs.joinmastodon.org/entities/notification/
export const NotificationRecord = ImmutableRecord({ export const NotificationRecord = ImmutableRecord({
account: null, account: null as EmbeddedEntity<Account>,
chat_message: null, // pleroma:chat_mention chat_message: null as ImmutableMap<string, any> | string | null, // pleroma:chat_mention
created_at: new Date(), created_at: new Date(),
emoji: null, // pleroma:emoji_reaction emoji: null as string | null, // pleroma:emoji_reaction
id: '', id: '',
status: null, status: null as EmbeddedEntity<Status>,
target: null, // move target: null as EmbeddedEntity<Account>, // move
type: '', type: '' as NotificationType,
}); });
export const normalizeNotification = (notification: Record<string, any>) => { export const normalizeNotification = (notification: Record<string, any>) => {

View File

@ -16,13 +16,14 @@ import { normalizeEmoji } from 'soapbox/normalizers/emoji';
import { normalizeMention } from 'soapbox/normalizers/mention'; import { normalizeMention } from 'soapbox/normalizers/mention';
import { normalizePoll } from 'soapbox/normalizers/poll'; import { normalizePoll } from 'soapbox/normalizers/poll';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
import type { Account, Attachment, Card, Emoji, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities'; import type { Account, Attachment, Card, Emoji, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct'; type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct';
// https://docs.joinmastodon.org/entities/status/ // https://docs.joinmastodon.org/entities/status/
export const StatusRecord = ImmutableRecord({ export const StatusRecord = ImmutableRecord({
account: null as EmbeddedEntity<Account>, account: null as EmbeddedEntity<Account | ReducerAccount>,
application: null as ImmutableMap<string, any> | null, application: null as ImmutableMap<string, any> | null,
bookmarked: false, bookmarked: false,
card: null as Card | null, card: null as Card | null,

View File

@ -45,14 +45,18 @@ type AccountMap = ImmutableMap<string, any>;
type APIEntity = Record<string, any>; type APIEntity = Record<string, any>;
type APIEntities = Array<APIEntity>; type APIEntities = Array<APIEntity>;
type State = ImmutableMap<string | number, AccountRecord>; export interface ReducerAccount extends AccountRecord {
moved: string | null,
}
type State = ImmutableMap<string | number, ReducerAccount>;
const initialState: State = ImmutableMap(); const initialState: State = ImmutableMap();
const minifyAccount = (account: AccountRecord): AccountRecord => { const minifyAccount = (account: AccountRecord): ReducerAccount => {
return account.mergeWith((o, n) => n || o, { return account.mergeWith((o, n) => n || o, {
moved: normalizeId(account.getIn(['moved', 'id'])), moved: normalizeId(account.getIn(['moved', 'id'])),
}); }) as ReducerAccount;
}; };
const fixAccount = (state: State, account: APIEntity) => { const fixAccount = (state: State, account: APIEntity) => {
@ -194,9 +198,9 @@ const importAdminUser = (state: State, adminUser: ImmutableMap<string, any>): St
const account = state.get(id); const account = state.get(id);
if (!account) { if (!account) {
return state.set(id, buildAccount(adminUser)); return state.set(id, minifyAccount(buildAccount(adminUser)));
} else { } else {
return state.set(id, mergeAdminUser(account, adminUser)); return state.set(id, minifyAccount(mergeAdminUser(account, adminUser)));
} }
}; };
@ -223,7 +227,7 @@ export default function accounts(state: State = initialState, action: AnyAction)
case ACCOUNTS_IMPORT: case ACCOUNTS_IMPORT:
return normalizeAccounts(state, action.accounts); return normalizeAccounts(state, action.accounts);
case ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP: case ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP:
return state.set(-1, normalizeAccount({ username: action.username })); return fixAccount(state, { id: -1, username: action.username });
case CHATS_FETCH_SUCCESS: case CHATS_FETCH_SUCCESS:
case CHATS_EXPAND_SUCCESS: case CHATS_EXPAND_SUCCESS:
return importAccountsFromChats(state, action.chats); return importAccountsFromChats(state, action.chats);

View File

@ -1,12 +0,0 @@
import { List as ImmutableList, fromJS } from 'immutable';
import { FILTERS_FETCH_SUCCESS } from '../actions/filters';
export default function filters(state = ImmutableList(), action) {
switch(action.type) {
case FILTERS_FETCH_SUCCESS:
return fromJS(action.filters);
default:
return state;
}
}

View File

@ -0,0 +1,25 @@
import {
Map as ImmutableMap,
List as ImmutableList,
fromJS,
} from 'immutable';
import { FILTERS_FETCH_SUCCESS } from '../actions/filters';
import type { AnyAction } from 'redux';
type Filter = ImmutableMap<string, any>;
type State = ImmutableList<Filter>;
const importFilters = (_state: State, filters: unknown): State => {
return ImmutableList(fromJS(filters)).map(filter => ImmutableMap(fromJS(filter)));
};
export default function filters(state: State = ImmutableList<Filter>(), action: AnyAction): State {
switch(action.type) {
case FILTERS_FETCH_SUCCESS:
return importFilters(state, action.filters);
default:
return state;
}
}

View File

@ -39,15 +39,22 @@ type StatusRecord = ReturnType<typeof normalizeStatus>;
type APIEntity = Record<string, any>; type APIEntity = Record<string, any>;
type APIEntities = Array<APIEntity>; type APIEntities = Array<APIEntity>;
type State = ImmutableMap<string, StatusRecord>; type State = ImmutableMap<string, ReducerStatus>;
const minifyStatus = (status: StatusRecord): StatusRecord => { export interface ReducerStatus extends StatusRecord {
account: string | null,
reblog: string | null,
poll: string | null,
quote: string | null,
}
const minifyStatus = (status: StatusRecord): ReducerStatus => {
return status.mergeWith((o, n) => n || o, { return status.mergeWith((o, n) => n || o, {
account: normalizeId(status.getIn(['account', 'id'])), account: normalizeId(status.getIn(['account', 'id'])),
reblog: normalizeId(status.getIn(['reblog', 'id'])), reblog: normalizeId(status.getIn(['reblog', 'id'])),
poll: normalizeId(status.getIn(['poll', 'id'])), poll: normalizeId(status.getIn(['poll', 'id'])),
quote: normalizeId(status.getIn(['quote', 'id'])), quote: normalizeId(status.getIn(['quote', 'id'])),
}); }) as ReducerStatus;
}; };
// Gets titles of poll options from status // Gets titles of poll options from status
@ -121,14 +128,14 @@ const fixQuote = (status: StatusRecord, oldStatus?: StatusRecord): StatusRecord
} }
}; };
const fixStatus = (state: State, status: APIEntity, expandSpoilers: boolean): StatusRecord => { const fixStatus = (state: State, status: APIEntity, expandSpoilers: boolean): ReducerStatus => {
const oldStatus = state.get(status.id); const oldStatus = state.get(status.id);
return normalizeStatus(status).withMutations(status => { return normalizeStatus(status).withMutations(status => {
fixQuote(status, oldStatus); fixQuote(status, oldStatus);
calculateStatus(status, oldStatus, expandSpoilers); calculateStatus(status, oldStatus, expandSpoilers);
minifyStatus(status); minifyStatus(status);
}); }) as ReducerStatus;
}; };
const importStatus = (state: State, status: APIEntity, expandSpoilers: boolean): State => const importStatus = (state: State, status: APIEntity, expandSpoilers: boolean): State =>

View File

@ -1,332 +0,0 @@
import {
Map as ImmutableMap,
List as ImmutableList,
OrderedSet as ImmutableOrderedSet,
} from 'immutable';
import { createSelector } from 'reselect';
import { getSettings } from 'soapbox/actions/settings';
import { getDomain } from 'soapbox/utils/accounts';
import { validId } from 'soapbox/utils/auth';
import ConfigDB from 'soapbox/utils/config_db';
import { shouldFilter } from 'soapbox/utils/timelines';
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null);
const getAccountMoved = (state, id) => state.getIn(['accounts', state.getIn(['accounts', id, 'moved'])]);
const getAccountMeta = (state, id) => state.getIn(['accounts_meta', id], ImmutableMap());
const getAccountAdminData = (state, id) => state.getIn(['admin', 'users', id]);
const getAccountPatron = (state, id) => {
const url = state.getIn(['accounts', id, 'url']);
return state.getIn(['patron', 'accounts', url]);
};
export const makeGetAccount = () => {
return createSelector([
getAccountBase,
getAccountCounters,
getAccountRelationship,
getAccountMoved,
getAccountMeta,
getAccountAdminData,
getAccountPatron,
], (base, counters, relationship, moved, meta, admin, patron) => {
if (base === null) {
return null;
}
return base.withMutations(map => {
map.merge(counters);
map.merge(meta);
map.set('pleroma', meta.get('pleroma', ImmutableMap()).merge(base.get('pleroma', ImmutableMap()))); // Lol, thanks Pleroma
map.set('relationship', relationship);
map.set('moved', moved);
map.set('patron', patron);
map.setIn(['pleroma', 'admin'], admin);
});
});
};
const findAccountsByUsername = (state, username) => {
const accounts = state.get('accounts');
return accounts.filter(account => {
return username.toLowerCase() === account.getIn(['acct'], '').toLowerCase();
});
};
export const findAccountByUsername = (state, username) => {
const accounts = findAccountsByUsername(state, username);
if (accounts.size > 1) {
const me = state.get('me');
const meURL = state.getIn(['accounts', me, 'url']);
return accounts.find(account => {
try {
// If more than one account has the same username, try matching its host
const { host } = new URL(account.get('url'));
const { host: meHost } = new URL(meURL);
return host === meHost;
} catch {
return false;
}
});
} else {
return accounts.first();
}
};
const toServerSideType = columnType => {
switch (columnType) {
case 'home':
case 'notifications':
case 'public':
case 'thread':
return columnType;
default:
if (columnType.indexOf('list:') > -1) {
return 'home';
} else {
return 'public'; // community, account, hashtag
}
}
};
export const getFilters = (state, { contextType }) => state.get('filters', ImmutableList()).filter(filter => contextType && filter.get('context').includes(toServerSideType(contextType)) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date())));
const escapeRegExp = string =>
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
export const regexFromFilters = filters => {
if (filters.size === 0) {
return null;
}
return new RegExp(filters.map(filter => {
let expr = escapeRegExp(filter.get('phrase'));
if (filter.get('whole_word')) {
if (/^[\w]/.test(expr)) {
expr = `\\b${expr}`;
}
if (/[\w]$/.test(expr)) {
expr = `${expr}\\b`;
}
}
return expr;
}).join('|'), 'i');
};
export const makeGetStatus = () => {
return createSelector(
[
(state, { id }) => state.getIn(['statuses', id]),
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
(state, { username }) => username,
getFilters,
(state) => state.get('me'),
],
(statusBase, statusReblog, accountBase, accountReblog, username, filters, me) => {
if (!statusBase) {
return null;
}
const accountUsername = accountBase.get('acct');
//Must be owner of status if username exists
if (accountUsername !== username && username !== undefined) {
return null;
}
if (statusReblog) {
statusReblog = statusReblog.set('account', accountReblog);
} else {
statusReblog = null;
}
const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(filters);
const filtered = regex && regex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'));
return statusBase.withMutations(map => {
map.set('reblog', statusReblog);
map.set('account', accountBase);
map.set('filtered', filtered);
});
},
);
};
const getAlertsBase = state => state.get('alerts');
export const getAlerts = createSelector([getAlertsBase], (base) => {
const arr = [];
base.forEach(item => {
arr.push({
message: item.get('message'),
title: item.get('title'),
actionLabel: item.get('actionLabel'),
actionLink: item.get('actionLink'),
key: item.get('key'),
className: `notification-bar-${item.get('severity', 'info')}`,
activeClassName: 'snackbar--active',
dismissAfter: 6000,
style: false,
});
});
return arr;
});
export const makeGetNotification = () => {
return createSelector([
(state, notification) => notification,
(state, notification) => state.getIn(['accounts', notification.get('account')]),
(state, notification) => state.getIn(['accounts', notification.get('target')]),
(state, notification) => state.getIn(['statuses', notification.get('status')]),
], (notification, account, target, status) => {
return notification.merge({ account, target, status });
});
};
export const getAccountGallery = createSelector([
(state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
state => state.get('statuses'),
state => state.get('accounts'),
], (statusIds, statuses, accounts) => {
return statusIds.reduce((medias, statusId) => {
const status = statuses.get(statusId);
const account = accounts.get(status.get('account'));
if (status.get('reblog')) return medias;
return medias.concat(status.get('media_attachments')
.map(media => media.merge({ status, account })));
}, ImmutableList());
});
export const makeGetChat = () => {
return createSelector(
[
(state, { id }) => state.getIn(['chats', 'items', id]),
(state, { id }) => state.getIn(['accounts', state.getIn(['chats', 'items', id, 'account'])]),
(state, { last_message }) => state.getIn(['chat_messages', last_message]),
],
(chat, account, lastMessage) => {
if (!chat) return null;
return chat.withMutations(map => {
map.set('account', account);
map.set('last_message', lastMessage);
});
},
);
};
export const makeGetReport = () => {
const getStatus = makeGetStatus();
return createSelector(
[
(state, id) => state.getIn(['admin', 'reports', id]),
(state, id) => state.getIn(['admin', 'reports', id, 'statuses']).map(
statusId => state.getIn(['statuses', statusId]))
.filter(s => s)
.map(s => getStatus(state, s.toJS())),
],
(report, statuses) => {
if (!report) return null;
return report.set('statuses', statuses);
},
);
};
const getAuthUserIds = createSelector([
state => state.getIn(['auth', 'users'], ImmutableMap()),
], authUsers => {
return authUsers.reduce((ids, authUser) => {
try {
const id = authUser.get('id');
return validId(id) ? ids.add(id) : ids;
} catch {
return ids;
}
}, ImmutableOrderedSet());
});
export const makeGetOtherAccounts = () => {
return createSelector([
state => state.get('accounts'),
getAuthUserIds,
state => state.get('me'),
],
(accounts, authUserIds, me) => {
return authUserIds
.reduce((list, id) => {
if (id === me) return list;
const account = accounts.get(id);
return account ? list.push(account) : list;
}, ImmutableList());
});
};
const getSimplePolicy = createSelector([
state => state.getIn(['admin', 'configs'], ImmutableMap()),
state => state.getIn(['instance', 'pleroma', 'metadata', 'federation', 'mrf_simple'], ImmutableMap()),
], (configs, instancePolicy) => {
return instancePolicy.merge(ConfigDB.toSimplePolicy(configs));
});
const getRemoteInstanceFavicon = (state, host) => (
state.get('accounts')
.find(account => getDomain(account) === host, null, ImmutableMap())
.getIn(['pleroma', 'favicon'])
);
const getRemoteInstanceFederation = (state, host) => (
getSimplePolicy(state)
.map(hosts => hosts.includes(host))
);
export const makeGetHosts = () => {
return createSelector([getSimplePolicy], (simplePolicy) => {
return simplePolicy
.deleteAll(['accept', 'reject_deletes', 'report_removal'])
.reduce((acc, hosts) => acc.union(hosts), ImmutableOrderedSet())
.sort();
});
};
export const makeGetRemoteInstance = () => {
return createSelector([
(state, host) => host,
getRemoteInstanceFavicon,
getRemoteInstanceFederation,
], (host, favicon, federation) => {
return ImmutableMap({
host,
favicon,
federation,
});
});
};
export const makeGetStatusIds = () => createSelector([
(state, { type, prefix }) => getSettings(state).get(prefix || type, ImmutableMap()),
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableOrderedSet()),
(state) => state.get('statuses'),
(state) => state.get('me'),
], (columnSettings, statusIds, statuses, me) => {
return statusIds.filter(id => {
const status = statuses.get(id);
if (!status) return true;
return !shouldFilter(status, columnSettings);
});
});

View File

@ -0,0 +1,359 @@
import {
Map as ImmutableMap,
List as ImmutableList,
OrderedSet as ImmutableOrderedSet,
} from 'immutable';
import { createSelector } from 'reselect';
import { getSettings } from 'soapbox/actions/settings';
import { getDomain } from 'soapbox/utils/accounts';
import { validId } from 'soapbox/utils/auth';
import ConfigDB from 'soapbox/utils/config_db';
import { shouldFilter } from 'soapbox/utils/timelines';
import type { RootState } from 'soapbox/store';
import type { Notification } from 'soapbox/types/entities';
const normalizeId = (id: any): string => typeof id === 'string' ? id : '';
const getAccountBase = (state: RootState, id: string) => state.accounts.get(id);
const getAccountCounters = (state: RootState, id: string) => state.accounts_counters.get(id);
const getAccountRelationship = (state: RootState, id: string) => state.relationships.get(id);
const getAccountMoved = (state: RootState, id: string) => state.accounts.get(state.accounts.get(id)?.moved || '');
const getAccountMeta = (state: RootState, id: string) => state.accounts_meta.get(id, ImmutableMap());
const getAccountAdminData = (state: RootState, id: string) => state.admin.users.get(id);
const getAccountPatron = (state: RootState, id: string) => {
const url = state.accounts.get(id)?.url;
return state.patron.getIn(['accounts', url]);
};
export const makeGetAccount = () => {
return createSelector([
getAccountBase,
getAccountCounters,
getAccountRelationship,
getAccountMoved,
getAccountMeta,
getAccountAdminData,
getAccountPatron,
], (base, counters, relationship, moved, meta, admin, patron) => {
if (!base) return null;
return base.withMutations(map => {
map.merge(counters);
map.merge(meta);
map.set('pleroma', meta.get('pleroma', ImmutableMap()).merge(base.get('pleroma', ImmutableMap()))); // Lol, thanks Pleroma
map.set('relationship', relationship);
map.set('moved', moved || null);
map.set('patron', patron);
map.setIn(['pleroma', 'admin'], admin);
});
});
};
const findAccountsByUsername = (state: RootState, username: string) => {
const accounts = state.accounts;
return accounts.filter(account => {
return username.toLowerCase() === account.acct.toLowerCase();
});
};
export const findAccountByUsername = (state: RootState, username: string) => {
const accounts = findAccountsByUsername(state, username);
if (accounts.size > 1) {
const me = state.me;
const meURL = state.accounts.get(me)?.url || '';
return accounts.find(account => {
try {
// If more than one account has the same username, try matching its host
const { host } = new URL(account.url);
const { host: meHost } = new URL(meURL);
return host === meHost;
} catch {
return false;
}
});
} else {
return accounts.first();
}
};
const toServerSideType = (columnType: string): string => {
switch (columnType) {
case 'home':
case 'notifications':
case 'public':
case 'thread':
return columnType;
default:
if (columnType.indexOf('list:') > -1) {
return 'home';
} else {
return 'public'; // community, account, hashtag
}
}
};
type FilterContext = { contextType: string };
export const getFilters = (state: RootState, { contextType }: FilterContext) => {
return state.filters.filter((filter): boolean => {
return contextType
&& filter.get('context').includes(toServerSideType(contextType))
&& (filter.get('expires_at') === null
|| Date.parse(filter.get('expires_at')) > new Date().getTime());
});
};
const escapeRegExp = (string: string) =>
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
export const regexFromFilters = (filters: ImmutableList<ImmutableMap<string, any>>) => {
if (filters.size === 0) return null;
return new RegExp(filters.map(filter => {
let expr = escapeRegExp(filter.get('phrase'));
if (filter.get('whole_word')) {
if (/^[\w]/.test(expr)) {
expr = `\\b${expr}`;
}
if (/[\w]$/.test(expr)) {
expr = `${expr}\\b`;
}
}
return expr;
}).join('|'), 'i');
};
type APIStatus = { id: string, username: string };
export const makeGetStatus = () => {
return createSelector(
[
(state: RootState, { id }: APIStatus) => state.statuses.get(id),
(state: RootState, { id }: APIStatus) => state.statuses.get(state.statuses.get(id)?.reblog || ''),
(state: RootState, { id }: APIStatus) => state.accounts.get(state.statuses.get(id)?.account || ''),
(state: RootState, { id }: APIStatus) => state.accounts.get(state.statuses.get(state.statuses.get(id)?.reblog || '')?.account || ''),
(_state: RootState, { username }: APIStatus) => username,
getFilters,
(state: RootState) => state.me,
],
(statusBase, statusReblog, accountBase, accountReblog, username, filters, me) => {
if (!statusBase || !accountBase) return null;
const accountUsername = accountBase.acct;
//Must be owner of status if username exists
if (accountUsername !== username && username !== undefined) {
return null;
}
if (statusReblog && accountReblog) {
// @ts-ignore AAHHHHH
statusReblog = statusReblog.set('account', accountReblog);
} else {
statusReblog = undefined;
}
const regex = (accountReblog || accountBase).id !== me && regexFromFilters(filters);
const filtered = regex && regex.test(statusReblog?.search_index || statusBase.search_index);
return statusBase.withMutations(map => {
map.set('reblog', statusReblog || null);
// @ts-ignore :(
map.set('account', accountBase || null);
map.set('filtered', Boolean(filtered));
});
},
);
};
const getAlertsBase = (state: RootState) => state.alerts;
const buildAlert = (item: any) => {
return {
message: item.message,
title: item.title,
actionLabel: item.actionLabel,
actionLink: item.actionLink,
key: item.key,
className: `notification-bar-${item.severity}`,
activeClassName: 'snackbar--active',
dismissAfter: 6000,
style: false,
};
};
type Alert = ReturnType<typeof buildAlert>;
export const getAlerts = createSelector([getAlertsBase], (base): Alert[] => {
const arr: Alert[] = [];
base.forEach((item: any) => arr.push(buildAlert(item)));
return arr;
});
export const makeGetNotification = () => {
return createSelector([
(_state: RootState, notification: Notification) => notification,
(state: RootState, notification: Notification) => state.accounts.get(normalizeId(notification.account)),
(state: RootState, notification: Notification) => state.accounts.get(normalizeId(notification.target)),
(state: RootState, notification: Notification) => state.statuses.get(normalizeId(notification.status)),
], (notification, account, target, status) => {
return notification.merge({
// @ts-ignore
account: account || null,
// @ts-ignore
target: target || null,
// @ts-ignore
status: status || null,
});
});
};
export const getAccountGallery = createSelector([
(state: RootState, id: string) => state.timelines.getIn([`account:${id}:media`, 'items'], ImmutableList()),
(state: RootState) => state.statuses,
(state: RootState) => state.accounts,
], (statusIds, statuses, accounts) => {
return statusIds.reduce((medias: ImmutableList<any>, statusId: string) => {
const status = statuses.get(statusId);
if (!status) return medias;
if (status.reblog) return medias;
if (typeof status.account !== 'string') return medias;
const account = accounts.get(status.account);
return medias.concat(
status.media_attachments.map(media => media.merge({ status, account })));
}, ImmutableList());
});
type APIChat = { id: string, last_message: string };
export const makeGetChat = () => {
return createSelector(
[
(state: RootState, { id }: APIChat) => state.chats.getIn(['items', id]),
(state: RootState, { id }: APIChat) => state.accounts.get(state.chats.getIn(['items', id, 'account'])),
(state: RootState, { last_message }: APIChat) => state.chat_messages.get(last_message),
],
(chat, account, lastMessage: string) => {
if (!chat) return null;
return chat.withMutations((map: ImmutableMap<string, any>) => {
map.set('account', account);
map.set('last_message', lastMessage);
});
},
);
};
export const makeGetReport = () => {
const getStatus = makeGetStatus();
return createSelector(
[
(state: RootState, id: string) => state.admin.getIn(['reports', id]),
(state: RootState, id: string) => state.admin.getIn(['reports', id, 'statuses']).map(
(statusId: string) => state.statuses.get(statusId))
.filter((s: any) => s)
.map((s: any) => getStatus(state, s.toJS())),
],
(report, statuses) => {
if (!report) return null;
return report.set('statuses', statuses);
},
);
};
const getAuthUserIds = createSelector([
(state: RootState) => state.auth.get('users', ImmutableMap()),
], authUsers => {
return authUsers.reduce((ids: ImmutableOrderedSet<string>, authUser: ImmutableMap<string, any>) => {
try {
const id = authUser.get('id');
return validId(id) ? ids.add(id) : ids;
} catch {
return ids;
}
}, ImmutableOrderedSet());
});
export const makeGetOtherAccounts = () => {
return createSelector([
(state: RootState) => state.accounts,
getAuthUserIds,
(state: RootState) => state.me,
],
(accounts, authUserIds, me) => {
return authUserIds
.reduce((list: ImmutableList<any>, id: string) => {
if (id === me) return list;
const account = accounts.get(id);
return account ? list.push(account) : list;
}, ImmutableList());
});
};
const getSimplePolicy = createSelector([
(state: RootState) => state.admin.configs,
(state: RootState) => state.instance.pleroma.getIn(['metadata', 'federation', 'mrf_simple'], ImmutableMap()),
], (configs, instancePolicy: ImmutableMap<string, any>) => {
return instancePolicy.merge(ConfigDB.toSimplePolicy(configs));
});
const getRemoteInstanceFavicon = (state: RootState, host: string) => (
(state.accounts.find(account => getDomain(account) === host, null) || ImmutableMap())
.getIn(['pleroma', 'favicon'])
);
const getRemoteInstanceFederation = (state: RootState, host: string) => (
getSimplePolicy(state)
.map(hosts => hosts.includes(host))
);
export const makeGetHosts = () => {
return createSelector([getSimplePolicy], (simplePolicy) => {
return simplePolicy
.deleteAll(['accept', 'reject_deletes', 'report_removal'])
.reduce((acc, hosts) => acc.union(hosts), ImmutableOrderedSet())
.sort();
});
};
export const makeGetRemoteInstance = () => {
return createSelector([
(_state: RootState, host: string) => host,
getRemoteInstanceFavicon,
getRemoteInstanceFederation,
], (host, favicon, federation) => {
return ImmutableMap({
host,
favicon,
federation,
});
});
};
type ColumnQuery = { type: string, prefix?: string };
export const makeGetStatusIds = () => createSelector([
(state: RootState, { type, prefix }: ColumnQuery) => getSettings(state).get(prefix || type, ImmutableMap()),
(state: RootState, { type }: ColumnQuery) => state.timelines.getIn([type, 'items'], ImmutableOrderedSet()),
(state: RootState) => state.statuses,
], (columnSettings, statusIds: string[], statuses) => {
return statusIds.filter((id: string) => {
const status = statuses.get(id);
if (!status) return true;
return !shouldFilter(status, columnSettings);
});
});

View File

@ -2,7 +2,6 @@ import { fromJS } from 'immutable';
import { import {
getDomain, getDomain,
acctFull,
isStaff, isStaff,
isAdmin, isAdmin,
isModerator, isModerator,
@ -18,28 +17,6 @@ describe('getDomain', () => {
}); });
}); });
describe('acctFull', () => {
describe('with a local user', () => {
const account = fromJS({
acct: 'alice',
url: 'https://party.com/users/alice',
});
it('returns the full acct', () => {
expect(acctFull(account)).toEqual('alice@party.com');
});
});
describe('with a remote user', () => {
const account = fromJS({
acct: 'bob@pool.com',
url: 'https://pool.com/users/bob',
});
it('returns the full acct', () => {
expect(acctFull(account)).toEqual('bob@pool.com');
});
});
});
describe('isStaff', () => { describe('isStaff', () => {
describe('with empty user', () => { describe('with empty user', () => {
const account = fromJS({}); const account = fromJS({});

View File

@ -2,26 +2,20 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl
import { Account } from 'soapbox/types/entities'; import { Account } from 'soapbox/types/entities';
const getDomainFromURL = (account: ImmutableMap<string, any>): string => { const getDomainFromURL = (account: Account): string => {
try { try {
const url = account.get('url'); const url = account.url;
return new URL(url).host; return new URL(url).host;
} catch { } catch {
return ''; return '';
} }
}; };
export const getDomain = (account: ImmutableMap<string, any>): string => { export const getDomain = (account: Account): string => {
const domain = account.get('acct', '').split('@')[1]; const domain = account.acct.split('@')[1];
return domain ? domain : getDomainFromURL(account); return domain ? domain : getDomainFromURL(account);
}; };
export const guessFqn = (account: ImmutableMap<string, any>): string => {
const [user, domain] = account.get('acct', '').split('@');
if (!domain) return [user, getDomainFromURL(account)].join('@');
return account.get('acct', '');
};
export const getBaseURL = (account: ImmutableMap<string, any>): string => { export const getBaseURL = (account: ImmutableMap<string, any>): string => {
try { try {
const url = account.get('url'); const url = account.get('url');
@ -31,11 +25,6 @@ export const getBaseURL = (account: ImmutableMap<string, any>): string => {
} }
}; };
// user@domain even for local users
export const acctFull = (account: ImmutableMap<string, any>): string => (
account.get('fqn') || guessFqn(account) || ''
);
export const getAcct = (account: Account, displayFqn: boolean): string => ( export const getAcct = (account: Account, displayFqn: boolean): string => (
displayFqn === true ? account.fqn : account.acct displayFqn === true ? account.fqn : account.acct
); );