Merge pull request #2517 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 3bf896c973
th-downstream
commit
c8fe36c349
@ -1,5 +1,5 @@
|
|||||||
# Node.js
|
# In test, compile the NodeJS code as if we are in production
|
||||||
NODE_ENV=tests
|
NODE_ENV=production
|
||||||
# Federation
|
# Federation
|
||||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||||
LOCAL_HTTPS=true
|
LOCAL_HTTPS=true
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import 'core-js/features/object/assign';
|
|
||||||
import 'core-js/features/object/values';
|
|
||||||
import 'core-js/features/symbol';
|
|
||||||
import 'core-js/features/promise/finally';
|
|
||||||
import { decode as decodeBase64 } from '../utils/base64';
|
|
||||||
|
|
||||||
if (!Object.hasOwn(HTMLCanvasElement.prototype, 'toBlob')) {
|
|
||||||
const BASE64_MARKER = ';base64,';
|
|
||||||
|
|
||||||
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
|
|
||||||
value: function (
|
|
||||||
this: HTMLCanvasElement,
|
|
||||||
callback: BlobCallback,
|
|
||||||
type = 'image/png',
|
|
||||||
quality: unknown,
|
|
||||||
) {
|
|
||||||
const dataURL: string = this.toDataURL(type, quality);
|
|
||||||
let data;
|
|
||||||
|
|
||||||
if (dataURL.includes(BASE64_MARKER)) {
|
|
||||||
const [, base64] = dataURL.split(BASE64_MARKER);
|
|
||||||
data = decodeBase64(base64);
|
|
||||||
} else {
|
|
||||||
[, data] = dataURL.split(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(new Blob([data], { type }));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,2 +1 @@
|
|||||||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
|
||||||
import 'requestidlecallback';
|
import 'requestidlecallback';
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
|
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||||
|
|
||||||
|
export const revealAccount = createAction<{
|
||||||
|
id: string;
|
||||||
|
}>('accounts/revealAccount');
|
||||||
|
|
||||||
|
export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
|
||||||
|
'accounts/importAccounts',
|
||||||
|
);
|
||||||
|
|
||||||
|
function actionWithSkipLoadingTrue<Args extends object>(args: Args) {
|
||||||
|
return {
|
||||||
|
payload: {
|
||||||
|
...args,
|
||||||
|
skipLoading: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const followAccountSuccess = createAction(
|
||||||
|
'accounts/followAccountSuccess',
|
||||||
|
actionWithSkipLoadingTrue<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
alreadyFollowing: boolean;
|
||||||
|
}>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const unfollowAccountSuccess = createAction(
|
||||||
|
'accounts/unfollowAccountSuccess',
|
||||||
|
actionWithSkipLoadingTrue<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
statuses: unknown;
|
||||||
|
alreadyFollowing?: boolean;
|
||||||
|
}>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const authorizeFollowRequestSuccess = createAction<{ id: string }>(
|
||||||
|
'accounts/followRequestAuthorizeSuccess',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const rejectFollowRequestSuccess = createAction<{ id: string }>(
|
||||||
|
'accounts/followRequestRejectSuccess',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const followAccountRequest = createAction(
|
||||||
|
'accounts/followRequest',
|
||||||
|
actionWithSkipLoadingTrue<{ id: string; locked: boolean }>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const followAccountFail = createAction(
|
||||||
|
'accounts/followFail',
|
||||||
|
actionWithSkipLoadingTrue<{ id: string; error: string; locked: boolean }>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const unfollowAccountRequest = createAction(
|
||||||
|
'accounts/unfollowRequest',
|
||||||
|
actionWithSkipLoadingTrue<{ id: string }>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const unfollowAccountFail = createAction(
|
||||||
|
'accounts/unfollowFail',
|
||||||
|
actionWithSkipLoadingTrue<{ id: string; error: string }>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const blockAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
statuses: unknown;
|
||||||
|
}>('accounts/blockSuccess');
|
||||||
|
|
||||||
|
export const unblockAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
}>('accounts/unblockSuccess');
|
||||||
|
|
||||||
|
export const muteAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
statuses: unknown;
|
||||||
|
}>('accounts/muteSuccess');
|
||||||
|
|
||||||
|
export const unmuteAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
}>('accounts/unmuteSuccess');
|
||||||
|
|
||||||
|
export const pinAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
}>('accounts/pinSuccess');
|
||||||
|
|
||||||
|
export const unpinAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
}>('accounts/unpinSuccess');
|
||||||
|
|
||||||
|
export const fetchRelationshipsSuccess = createAction(
|
||||||
|
'relationships/fetchSuccess',
|
||||||
|
actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>,
|
||||||
|
);
|
@ -0,0 +1,13 @@
|
|||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
|
export const blockDomainSuccess = createAction<{
|
||||||
|
domain: string;
|
||||||
|
accounts: Account[];
|
||||||
|
}>('domain_blocks/blockSuccess');
|
||||||
|
|
||||||
|
export const unblockDomainSuccess = createAction<{
|
||||||
|
domain: string;
|
||||||
|
accounts: Account[];
|
||||||
|
}>('domain_blocks/unblockSuccess');
|
@ -0,0 +1,23 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const notificationsUpdate = createAction(
|
||||||
|
'notifications/update',
|
||||||
|
({
|
||||||
|
playSound,
|
||||||
|
...args
|
||||||
|
}: {
|
||||||
|
notification: FakeApiNotificationJSON;
|
||||||
|
usePendingItems: boolean;
|
||||||
|
playSound: boolean;
|
||||||
|
}) => ({
|
||||||
|
payload: args,
|
||||||
|
meta: { playSound: playSound ? { sound: 'boop' } : undefined },
|
||||||
|
}),
|
||||||
|
);
|
@ -1,40 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { revealAccount } from 'mastodon/actions/accounts';
|
|
||||||
import { Button } from 'mastodon/components/button';
|
|
||||||
import { domain } from 'mastodon/initial_state';
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
|
||||||
|
|
||||||
reveal () {
|
|
||||||
dispatch(revealAccount(accountId));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
class LimitedAccountHint extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
accountId: PropTypes.string.isRequired,
|
|
||||||
reveal: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { reveal } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='limited-account-hint'>
|
|
||||||
<p><FormattedMessage id='limited_account_hint.title' defaultMessage='This profile has been hidden by the moderators of {domain}.' values={{ domain }} /></p>
|
|
||||||
<Button onClick={reveal}><FormattedMessage id='limited_account_hint.action' defaultMessage='Show profile anyway' /></Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(() => {}, mapDispatchToProps)(LimitedAccountHint);
|
|
@ -0,0 +1,35 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { revealAccount } from 'mastodon/actions/accounts_typed';
|
||||||
|
import { Button } from 'mastodon/components/button';
|
||||||
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
export const LimitedAccountHint: React.FC<{ accountId: string }> = ({
|
||||||
|
accountId,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const reveal = useCallback(() => {
|
||||||
|
dispatch(revealAccount({ id: accountId }));
|
||||||
|
}, [dispatch, accountId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='limited-account-hint'>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='limited_account_hint.title'
|
||||||
|
defaultMessage='This profile has been hidden by the moderators of {domain}.'
|
||||||
|
values={{ domain }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<Button onClick={reveal}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='limited_account_hint.action'
|
||||||
|
defaultMessage='Show profile anyway'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,149 @@
|
|||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
import { List, Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiAccountFieldJSON,
|
||||||
|
ApiAccountRoleJSON,
|
||||||
|
ApiAccountJSON,
|
||||||
|
} from 'mastodon/api_types/accounts';
|
||||||
|
import type { ApiCustomEmojiJSON } from 'mastodon/api_types/custom_emoji';
|
||||||
|
import emojify from 'mastodon/features/emoji/emoji';
|
||||||
|
import { unescapeHTML } from 'mastodon/utils/html';
|
||||||
|
|
||||||
|
import { CustomEmojiFactory } from './custom_emoji';
|
||||||
|
import type { CustomEmoji } from './custom_emoji';
|
||||||
|
|
||||||
|
// AccountField
|
||||||
|
interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
||||||
|
name_emojified: string;
|
||||||
|
value_emojified: string;
|
||||||
|
value_plain: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountField = RecordOf<AccountFieldShape>;
|
||||||
|
|
||||||
|
const AccountFieldFactory = ImmutableRecord<AccountFieldShape>({
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
verified_at: null,
|
||||||
|
name_emojified: '',
|
||||||
|
value_emojified: '',
|
||||||
|
value_plain: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// AccountRole
|
||||||
|
export type AccountRoleShape = ApiAccountRoleJSON;
|
||||||
|
export type AccountRole = RecordOf<AccountRoleShape>;
|
||||||
|
|
||||||
|
const AccountRoleFactory = ImmutableRecord<AccountRoleShape>({
|
||||||
|
color: '',
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Account
|
||||||
|
export interface AccountShape
|
||||||
|
extends Required<
|
||||||
|
Omit<ApiAccountJSON, 'emojis' | 'fields' | 'roles' | 'moved'>
|
||||||
|
> {
|
||||||
|
emojis: List<CustomEmoji>;
|
||||||
|
fields: List<AccountField>;
|
||||||
|
roles: List<AccountRole>;
|
||||||
|
display_name_html: string;
|
||||||
|
note_emojified: string;
|
||||||
|
note_plain: string | null;
|
||||||
|
hidden: boolean;
|
||||||
|
moved: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Account = RecordOf<AccountShape>;
|
||||||
|
|
||||||
|
export const accountDefaultValues: AccountShape = {
|
||||||
|
acct: '',
|
||||||
|
avatar: '',
|
||||||
|
avatar_static: '',
|
||||||
|
bot: false,
|
||||||
|
created_at: '',
|
||||||
|
discoverable: false,
|
||||||
|
display_name: '',
|
||||||
|
display_name_html: '',
|
||||||
|
emojis: List<CustomEmoji>(),
|
||||||
|
fields: List<AccountField>(),
|
||||||
|
group: false,
|
||||||
|
header: '',
|
||||||
|
header_static: '',
|
||||||
|
id: '',
|
||||||
|
last_status_at: '',
|
||||||
|
locked: false,
|
||||||
|
noindex: false,
|
||||||
|
note: '',
|
||||||
|
note_emojified: '',
|
||||||
|
note_plain: 'string',
|
||||||
|
roles: List<AccountRole>(),
|
||||||
|
uri: '',
|
||||||
|
url: '',
|
||||||
|
username: '',
|
||||||
|
followers_count: 0,
|
||||||
|
following_count: 0,
|
||||||
|
statuses_count: 0,
|
||||||
|
hidden: false,
|
||||||
|
suspended: false,
|
||||||
|
memorial: false,
|
||||||
|
limited: false,
|
||||||
|
moved: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);
|
||||||
|
|
||||||
|
type EmojiMap = Record<string, ApiCustomEmojiJSON>;
|
||||||
|
|
||||||
|
function makeEmojiMap(emojis: ApiCustomEmojiJSON[]) {
|
||||||
|
return emojis.reduce<EmojiMap>((obj, emoji) => {
|
||||||
|
obj[`:${emoji.shortcode}:`] = emoji;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAccountField(
|
||||||
|
jsonField: ApiAccountFieldJSON,
|
||||||
|
emojiMap: EmojiMap,
|
||||||
|
) {
|
||||||
|
return AccountFieldFactory({
|
||||||
|
...jsonField,
|
||||||
|
name_emojified: emojify(
|
||||||
|
escapeTextContentForBrowser(jsonField.name),
|
||||||
|
emojiMap,
|
||||||
|
),
|
||||||
|
value_emojified: emojify(jsonField.value, emojiMap),
|
||||||
|
value_plain: unescapeHTML(jsonField.value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
||||||
|
const { moved, ...accountJSON } = serverJSON;
|
||||||
|
|
||||||
|
const emojiMap = makeEmojiMap(accountJSON.emojis);
|
||||||
|
|
||||||
|
const displayName =
|
||||||
|
accountJSON.display_name.trim().length === 0
|
||||||
|
? accountJSON.username
|
||||||
|
: accountJSON.display_name;
|
||||||
|
|
||||||
|
return AccountFactory({
|
||||||
|
...accountJSON,
|
||||||
|
moved: moved?.id,
|
||||||
|
fields: List(
|
||||||
|
serverJSON.fields.map((field) => createAccountField(field, emojiMap)),
|
||||||
|
),
|
||||||
|
emojis: List(serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji))),
|
||||||
|
roles: List(serverJSON.roles?.map((role) => AccountRoleFactory(role))),
|
||||||
|
display_name_html: emojify(
|
||||||
|
escapeTextContentForBrowser(displayName),
|
||||||
|
emojiMap,
|
||||||
|
),
|
||||||
|
note_emojified: emojify(accountJSON.note, emojiMap),
|
||||||
|
note_plain: unescapeHTML(accountJSON.note),
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiCustomEmojiJSON } from 'mastodon/api_types/custom_emoji';
|
||||||
|
|
||||||
|
type CustomEmojiShape = Required<ApiCustomEmojiJSON>; // no changes from server shape
|
||||||
|
export type CustomEmoji = RecordOf<CustomEmojiShape>;
|
||||||
|
|
||||||
|
export const CustomEmojiFactory = Record<CustomEmojiShape>({
|
||||||
|
shortcode: '',
|
||||||
|
static_url: '',
|
||||||
|
url: '',
|
||||||
|
category: '',
|
||||||
|
visible_in_picker: false,
|
||||||
|
});
|
@ -0,0 +1,29 @@
|
|||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||||
|
|
||||||
|
type RelationshipShape = Required<ApiRelationshipJSON>; // no changes from server shape
|
||||||
|
export type Relationship = RecordOf<RelationshipShape>;
|
||||||
|
|
||||||
|
const RelationshipFactory = Record<RelationshipShape>({
|
||||||
|
blocked_by: false,
|
||||||
|
blocking: false,
|
||||||
|
domain_blocking: false,
|
||||||
|
endorsed: false,
|
||||||
|
followed_by: false,
|
||||||
|
following: false,
|
||||||
|
id: '',
|
||||||
|
languages: null,
|
||||||
|
muting_notifications: false,
|
||||||
|
muting: false,
|
||||||
|
note: '',
|
||||||
|
notifying: false,
|
||||||
|
requested_by: false,
|
||||||
|
requested: false,
|
||||||
|
showing_reblogs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createRelationship(attributes: Partial<RelationshipShape>) {
|
||||||
|
return RelationshipFactory(attributes);
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
import 'core-js/features/object/assign';
|
|
||||||
import 'core-js/features/object/values';
|
|
||||||
import 'core-js/features/symbol';
|
|
||||||
import 'core-js/features/promise/finally';
|
|
||||||
import { decode as decodeBase64 } from '../utils/base64';
|
|
||||||
|
|
||||||
if (!Object.hasOwn(HTMLCanvasElement.prototype, 'toBlob')) {
|
|
||||||
const BASE64_MARKER = ';base64,';
|
|
||||||
|
|
||||||
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
|
|
||||||
value: function (
|
|
||||||
this: HTMLCanvasElement,
|
|
||||||
callback: BlobCallback,
|
|
||||||
type = 'image/png',
|
|
||||||
quality: unknown,
|
|
||||||
) {
|
|
||||||
const dataURL: string = this.toDataURL(type, quality);
|
|
||||||
let data;
|
|
||||||
|
|
||||||
if (dataURL.includes(BASE64_MARKER)) {
|
|
||||||
const [, base64] = dataURL.split(BASE64_MARKER);
|
|
||||||
data = decodeBase64(base64);
|
|
||||||
} else {
|
|
||||||
[, data] = dataURL.split(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(new Blob([data], { type }));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,2 +1 @@
|
|||||||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
|
||||||
import 'requestidlecallback';
|
import 'requestidlecallback';
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
import { ACCOUNT_REVEAL } from 'mastodon/actions/accounts';
|
|
||||||
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from 'mastodon/actions/importer';
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
|
||||||
|
|
||||||
const normalizeAccount = (state, account) => {
|
|
||||||
account = { ...account };
|
|
||||||
|
|
||||||
delete account.followers_count;
|
|
||||||
delete account.following_count;
|
|
||||||
delete account.statuses_count;
|
|
||||||
|
|
||||||
account.hidden = state.getIn([account.id, 'hidden']) === false ? false : account.limited;
|
|
||||||
|
|
||||||
return state.set(account.id, fromJS(account));
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeAccounts = (state, accounts) => {
|
|
||||||
accounts.forEach(account => {
|
|
||||||
state = normalizeAccount(state, account);
|
|
||||||
});
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function accounts(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case ACCOUNT_IMPORT:
|
|
||||||
return normalizeAccount(state, action.account);
|
|
||||||
case ACCOUNTS_IMPORT:
|
|
||||||
return normalizeAccounts(state, action.accounts);
|
|
||||||
case ACCOUNT_REVEAL:
|
|
||||||
return state.setIn([action.id, 'hidden'], false);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,84 @@
|
|||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import type { Reducer } from 'redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
followAccountSuccess,
|
||||||
|
unfollowAccountSuccess,
|
||||||
|
importAccounts,
|
||||||
|
revealAccount,
|
||||||
|
} from 'mastodon/actions/accounts_typed';
|
||||||
|
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
import { createAccountFromServerJSON } from 'mastodon/models/account';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap<string, Account>();
|
||||||
|
|
||||||
|
const normalizeAccount = (
|
||||||
|
state: typeof initialState,
|
||||||
|
account: ApiAccountJSON,
|
||||||
|
) => {
|
||||||
|
return state.set(
|
||||||
|
account.id,
|
||||||
|
createAccountFromServerJSON(account).set(
|
||||||
|
'hidden',
|
||||||
|
state.get(account.id)?.hidden === false
|
||||||
|
? false
|
||||||
|
: account.limited || false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAccounts = (
|
||||||
|
state: typeof initialState,
|
||||||
|
accounts: ApiAccountJSON[],
|
||||||
|
) => {
|
||||||
|
accounts.forEach((account) => {
|
||||||
|
state = normalizeAccount(state, account);
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCurrentUser() {
|
||||||
|
if (!me)
|
||||||
|
throw new Error(
|
||||||
|
'No current user (me) defined when calling `accountsReducer`',
|
||||||
|
);
|
||||||
|
|
||||||
|
return me;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const accountsReducer: Reducer<typeof initialState> = (
|
||||||
|
state = initialState,
|
||||||
|
action,
|
||||||
|
) => {
|
||||||
|
if (revealAccount.match(action))
|
||||||
|
return state.setIn([action.payload.id, 'hidden'], false);
|
||||||
|
else if (importAccounts.match(action))
|
||||||
|
return normalizeAccounts(state, action.payload.accounts);
|
||||||
|
else if (followAccountSuccess.match(action)) {
|
||||||
|
return state
|
||||||
|
.update(
|
||||||
|
action.payload.relationship.id,
|
||||||
|
(account) => account?.update('followers_count', (n) => n + 1),
|
||||||
|
)
|
||||||
|
.update(
|
||||||
|
getCurrentUser(),
|
||||||
|
(account) => account?.update('following_count', (n) => n + 1),
|
||||||
|
);
|
||||||
|
} else if (unfollowAccountSuccess.match(action))
|
||||||
|
return state
|
||||||
|
.update(
|
||||||
|
action.payload.relationship.id,
|
||||||
|
(account) =>
|
||||||
|
account?.update('followers_count', (n) => Math.max(0, n - 1)),
|
||||||
|
)
|
||||||
|
.update(
|
||||||
|
getCurrentUser(),
|
||||||
|
(account) =>
|
||||||
|
account?.update('following_count', (n) => Math.max(0, n - 1)),
|
||||||
|
);
|
||||||
|
else return state;
|
||||||
|
};
|
@ -1,49 +0,0 @@
|
|||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
import { me } from 'mastodon/initial_state';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ACCOUNT_FOLLOW_SUCCESS,
|
|
||||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
|
||||||
} from '../actions/accounts';
|
|
||||||
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
|
|
||||||
|
|
||||||
const normalizeAccount = (state, account) => state.set(account.id, fromJS({
|
|
||||||
followers_count: account.followers_count,
|
|
||||||
following_count: account.following_count,
|
|
||||||
statuses_count: account.statuses_count,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const normalizeAccounts = (state, accounts) => {
|
|
||||||
accounts.forEach(account => {
|
|
||||||
state = normalizeAccount(state, account);
|
|
||||||
});
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
const incrementFollowers = (state, accountId) =>
|
|
||||||
state.updateIn([accountId, 'followers_count'], num => num + 1)
|
|
||||||
.updateIn([me, 'following_count'], num => num + 1);
|
|
||||||
|
|
||||||
const decrementFollowers = (state, accountId) =>
|
|
||||||
state.updateIn([accountId, 'followers_count'], num => Math.max(0, num - 1))
|
|
||||||
.updateIn([me, 'following_count'], num => Math.max(0, num - 1));
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
|
||||||
|
|
||||||
export default function accountsCounters(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case ACCOUNT_IMPORT:
|
|
||||||
return normalizeAccount(state, action.account);
|
|
||||||
case ACCOUNTS_IMPORT:
|
|
||||||
return normalizeAccounts(state, action.accounts);
|
|
||||||
case ACCOUNT_FOLLOW_SUCCESS:
|
|
||||||
return action.alreadyFollowing ? state :
|
|
||||||
incrementFollowers(state, action.relationship.id);
|
|
||||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
|
||||||
return decrementFollowers(state, action.relationship.id);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
import {
|
|
||||||
submitAccountNote,
|
|
||||||
} from '../actions/account_notes';
|
|
||||||
import {
|
|
||||||
ACCOUNT_FOLLOW_SUCCESS,
|
|
||||||
ACCOUNT_FOLLOW_REQUEST,
|
|
||||||
ACCOUNT_FOLLOW_FAIL,
|
|
||||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
|
||||||
ACCOUNT_UNFOLLOW_REQUEST,
|
|
||||||
ACCOUNT_UNFOLLOW_FAIL,
|
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
|
||||||
ACCOUNT_UNBLOCK_SUCCESS,
|
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
|
||||||
ACCOUNT_UNMUTE_SUCCESS,
|
|
||||||
ACCOUNT_PIN_SUCCESS,
|
|
||||||
ACCOUNT_UNPIN_SUCCESS,
|
|
||||||
RELATIONSHIPS_FETCH_SUCCESS,
|
|
||||||
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
|
||||||
FOLLOW_REQUEST_REJECT_SUCCESS,
|
|
||||||
} from '../actions/accounts';
|
|
||||||
import {
|
|
||||||
DOMAIN_BLOCK_SUCCESS,
|
|
||||||
DOMAIN_UNBLOCK_SUCCESS,
|
|
||||||
} from '../actions/domain_blocks';
|
|
||||||
import {
|
|
||||||
NOTIFICATIONS_UPDATE,
|
|
||||||
} from '../actions/notifications';
|
|
||||||
|
|
||||||
|
|
||||||
const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
|
|
||||||
|
|
||||||
const normalizeRelationships = (state, relationships) => {
|
|
||||||
relationships.forEach(relationship => {
|
|
||||||
state = normalizeRelationship(state, relationship);
|
|
||||||
});
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setDomainBlocking = (state, accounts, blocking) => {
|
|
||||||
return state.withMutations(map => {
|
|
||||||
accounts.forEach(id => {
|
|
||||||
map.setIn([id, 'domain_blocking'], blocking);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
|
||||||
|
|
||||||
export default function relationships(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
|
|
||||||
return state.setIn([action.id, 'followed_by'], true).setIn([action.id, 'requested_by'], false);
|
|
||||||
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
|
||||||
return state.setIn([action.id, 'followed_by'], false).setIn([action.id, 'requested_by'], false);
|
|
||||||
case NOTIFICATIONS_UPDATE:
|
|
||||||
return action.notification.type === 'follow_request' ? state.setIn([action.notification.account.id, 'requested_by'], true) : state;
|
|
||||||
case ACCOUNT_FOLLOW_REQUEST:
|
|
||||||
return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
|
|
||||||
case ACCOUNT_FOLLOW_FAIL:
|
|
||||||
return state.setIn([action.id, action.locked ? 'requested' : 'following'], false);
|
|
||||||
case ACCOUNT_UNFOLLOW_REQUEST:
|
|
||||||
return state.setIn([action.id, 'following'], false);
|
|
||||||
case ACCOUNT_UNFOLLOW_FAIL:
|
|
||||||
return state.setIn([action.id, 'following'], true);
|
|
||||||
case ACCOUNT_FOLLOW_SUCCESS:
|
|
||||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
|
||||||
case ACCOUNT_UNBLOCK_SUCCESS:
|
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
|
||||||
case ACCOUNT_UNMUTE_SUCCESS:
|
|
||||||
case ACCOUNT_PIN_SUCCESS:
|
|
||||||
case ACCOUNT_UNPIN_SUCCESS:
|
|
||||||
return normalizeRelationship(state, action.relationship);
|
|
||||||
case RELATIONSHIPS_FETCH_SUCCESS:
|
|
||||||
return normalizeRelationships(state, action.relationships);
|
|
||||||
case submitAccountNote.fulfilled:
|
|
||||||
return normalizeRelationship(state, action.payload.relationship);
|
|
||||||
case DOMAIN_BLOCK_SUCCESS:
|
|
||||||
return setDomainBlocking(state, action.accounts, true);
|
|
||||||
case DOMAIN_UNBLOCK_SUCCESS:
|
|
||||||
return setDomainBlocking(state, action.accounts, false);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,123 @@
|
|||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
import type { Reducer } from 'redux';
|
||||||
|
|
||||||
|
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||||
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
import { createRelationship } from 'mastodon/models/relationship';
|
||||||
|
import type { Relationship } from 'mastodon/models/relationship';
|
||||||
|
|
||||||
|
import { submitAccountNote } from '../actions/account_notes';
|
||||||
|
import {
|
||||||
|
followAccountSuccess,
|
||||||
|
unfollowAccountSuccess,
|
||||||
|
authorizeFollowRequestSuccess,
|
||||||
|
rejectFollowRequestSuccess,
|
||||||
|
followAccountRequest,
|
||||||
|
followAccountFail,
|
||||||
|
unfollowAccountRequest,
|
||||||
|
unfollowAccountFail,
|
||||||
|
blockAccountSuccess,
|
||||||
|
unblockAccountSuccess,
|
||||||
|
muteAccountSuccess,
|
||||||
|
unmuteAccountSuccess,
|
||||||
|
pinAccountSuccess,
|
||||||
|
unpinAccountSuccess,
|
||||||
|
fetchRelationshipsSuccess,
|
||||||
|
} from '../actions/accounts_typed';
|
||||||
|
import {
|
||||||
|
blockDomainSuccess,
|
||||||
|
unblockDomainSuccess,
|
||||||
|
} from '../actions/domain_blocks_typed';
|
||||||
|
import { notificationsUpdate } from '../actions/notifications_typed';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap<string, Relationship>();
|
||||||
|
type State = typeof initialState;
|
||||||
|
|
||||||
|
const normalizeRelationship = (
|
||||||
|
state: State,
|
||||||
|
relationship: ApiRelationshipJSON,
|
||||||
|
) => state.set(relationship.id, createRelationship(relationship));
|
||||||
|
|
||||||
|
const normalizeRelationships = (
|
||||||
|
state: State,
|
||||||
|
relationships: ApiRelationshipJSON[],
|
||||||
|
) => {
|
||||||
|
relationships.forEach((relationship) => {
|
||||||
|
state = normalizeRelationship(state, relationship);
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDomainBlocking = (
|
||||||
|
state: State,
|
||||||
|
accounts: Account[],
|
||||||
|
blocking: boolean,
|
||||||
|
) => {
|
||||||
|
return state.withMutations((map) => {
|
||||||
|
accounts.forEach((id) => {
|
||||||
|
map.setIn([id, 'domain_blocking'], blocking);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const relationshipsReducer: Reducer<State> = (
|
||||||
|
state = initialState,
|
||||||
|
action,
|
||||||
|
) => {
|
||||||
|
if (authorizeFollowRequestSuccess.match(action))
|
||||||
|
return state
|
||||||
|
.setIn([action.payload.id, 'followed_by'], true)
|
||||||
|
.setIn([action.payload.id, 'requested_by'], false);
|
||||||
|
else if (rejectFollowRequestSuccess.match(action))
|
||||||
|
return state
|
||||||
|
.setIn([action.payload.id, 'followed_by'], false)
|
||||||
|
.setIn([action.payload.id, 'requested_by'], false);
|
||||||
|
else if (notificationsUpdate.match(action))
|
||||||
|
return action.payload.notification.type === 'follow_request'
|
||||||
|
? state.setIn(
|
||||||
|
[action.payload.notification.account.id, 'requested_by'],
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
: state;
|
||||||
|
else if (followAccountRequest.match(action))
|
||||||
|
return state.getIn([action.payload.id, 'following'])
|
||||||
|
? state
|
||||||
|
: state.setIn(
|
||||||
|
[
|
||||||
|
action.payload.id,
|
||||||
|
action.payload.locked ? 'requested' : 'following',
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
else if (followAccountFail.match(action))
|
||||||
|
return state.setIn(
|
||||||
|
[action.payload.id, action.payload.locked ? 'requested' : 'following'],
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
else if (unfollowAccountRequest.match(action))
|
||||||
|
return state.setIn([action.payload.id, 'following'], false);
|
||||||
|
else if (unfollowAccountFail.match(action))
|
||||||
|
return state.setIn([action.payload.id, 'following'], true);
|
||||||
|
else if (
|
||||||
|
followAccountSuccess.match(action) ||
|
||||||
|
unfollowAccountSuccess.match(action) ||
|
||||||
|
blockAccountSuccess.match(action) ||
|
||||||
|
unblockAccountSuccess.match(action) ||
|
||||||
|
muteAccountSuccess.match(action) ||
|
||||||
|
unmuteAccountSuccess.match(action) ||
|
||||||
|
pinAccountSuccess.match(action) ||
|
||||||
|
unpinAccountSuccess.match(action) ||
|
||||||
|
isFulfilled(submitAccountNote)(action)
|
||||||
|
)
|
||||||
|
return normalizeRelationship(state, action.payload.relationship);
|
||||||
|
else if (fetchRelationshipsSuccess.match(action))
|
||||||
|
return normalizeRelationships(state, action.payload.relationships);
|
||||||
|
else if (blockDomainSuccess.match(action))
|
||||||
|
return setDomainBlocking(state, action.payload.accounts, true);
|
||||||
|
else if (unblockDomainSuccess.match(action))
|
||||||
|
return setDomainBlocking(state, action.payload.accounts, false);
|
||||||
|
else return state;
|
||||||
|
};
|
@ -0,0 +1,47 @@
|
|||||||
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { accountDefaultValues } from 'mastodon/models/account';
|
||||||
|
import type { Account, AccountShape } from 'mastodon/models/account';
|
||||||
|
import type { Relationship } from 'mastodon/models/relationship';
|
||||||
|
import type { RootState } from 'mastodon/store';
|
||||||
|
|
||||||
|
const getAccountBase = (state: RootState, id: string) =>
|
||||||
|
state.accounts.get(id, null);
|
||||||
|
|
||||||
|
const getAccountRelationship = (state: RootState, id: string) =>
|
||||||
|
state.relationships.get(id, null);
|
||||||
|
|
||||||
|
const getAccountMoved = (state: RootState, id: string) => {
|
||||||
|
const movedToId = state.accounts.get(id)?.moved;
|
||||||
|
|
||||||
|
if (!movedToId) return undefined;
|
||||||
|
|
||||||
|
return state.accounts.get(movedToId);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FullAccountShape extends Omit<AccountShape, 'moved'> {
|
||||||
|
relationship: Relationship | null;
|
||||||
|
moved: Account | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FullAccountFactory = ImmutableRecord<FullAccountShape>({
|
||||||
|
...accountDefaultValues,
|
||||||
|
moved: null,
|
||||||
|
relationship: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function makeGetAccount() {
|
||||||
|
return createSelector(
|
||||||
|
[getAccountBase, getAccountRelationship, getAccountMoved],
|
||||||
|
(base, relationship, moved) => {
|
||||||
|
if (base === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FullAccountFactory(base)
|
||||||
|
.set('relationship', relationship)
|
||||||
|
.set('moved', moved ?? null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
export function isDevelopment() {
|
||||||
|
return process.env.NODE_ENV === 'development';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProduction() {
|
||||||
|
return process.env.NODE_ENV === 'production';
|
||||||
|
}
|
@ -1,55 +0,0 @@
|
|||||||
import type { Record } from 'immutable';
|
|
||||||
|
|
||||||
type CustomEmoji = Record<{
|
|
||||||
shortcode: string;
|
|
||||||
static_url: string;
|
|
||||||
url: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
type AccountField = Record<{
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
verified_at: string | null;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
interface AccountApiResponseValues {
|
|
||||||
acct: string;
|
|
||||||
avatar: string;
|
|
||||||
avatar_static: string;
|
|
||||||
bot: boolean;
|
|
||||||
created_at: string;
|
|
||||||
discoverable: boolean;
|
|
||||||
display_name: string;
|
|
||||||
emojis: CustomEmoji[];
|
|
||||||
fields: AccountField[];
|
|
||||||
followers_count: number;
|
|
||||||
following_count: number;
|
|
||||||
group: boolean;
|
|
||||||
header: string;
|
|
||||||
header_static: string;
|
|
||||||
id: string;
|
|
||||||
last_status_at: string;
|
|
||||||
locked: boolean;
|
|
||||||
note: string;
|
|
||||||
statuses_count: number;
|
|
||||||
url: string;
|
|
||||||
uri: string;
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type NormalizedAccountField = Record<{
|
|
||||||
name_emojified: string;
|
|
||||||
value_emojified: string;
|
|
||||||
value_plain: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
interface NormalizedAccountValues {
|
|
||||||
display_name_html: string;
|
|
||||||
fields: NormalizedAccountField[];
|
|
||||||
note_emojified: string;
|
|
||||||
note_plain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Account = Record<
|
|
||||||
AccountApiResponseValues & NormalizedAccountValues
|
|
||||||
>;
|
|
@ -1,4 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative '../../lib/json_ld/security'
|
|
||||||
require_relative '../../lib/json_ld/identity'
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue