Merge remote-tracking branch 'soapbox/develop' into ts

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-07-13 20:57:12 +02:00
commit 479386af03
297 changed files with 2458 additions and 1950 deletions

View File

@ -57,7 +57,7 @@ lint-sass:
jest: jest:
stage: test stage: test
script: yarn test:coverage script: yarn test:coverage --runInBand
only: only:
changes: changes:
- "**/*.js" - "**/*.js"

View File

@ -1,18 +1,20 @@
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { mockStore } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import rootReducer from 'soapbox/reducers'; import { ReducerRecord, EditRecord } from 'soapbox/reducers/account_notes';
import { normalizeAccount } from '../../normalizers'; import { normalizeAccount, normalizeRelationship } from '../../normalizers';
import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes'; import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes';
import type { Account } from 'soapbox/types/entities';
describe('submitAccountNote()', () => { describe('submitAccountNote()', () => {
let store; let store: ReturnType<typeof mockStore>;
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('account_notes', { edit: { account: 1, comment: 'hello' } }); .set('account_notes', ReducerRecord({ edit: EditRecord({ account: '1', comment: 'hello' }) }));
store = mockStore(state); store = mockStore(state);
}); });
@ -60,11 +62,11 @@ describe('submitAccountNote()', () => {
}); });
describe('initAccountNoteModal()', () => { describe('initAccountNoteModal()', () => {
let store; let store: ReturnType<typeof mockStore>;
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('relationships', ImmutableMap({ 1: { note: 'hello' } })); .set('relationships', ImmutableMap({ '1': normalizeRelationship({ note: 'hello' }) }));
store = mockStore(state); store = mockStore(state);
}); });
@ -75,7 +77,7 @@ describe('initAccountNoteModal()', () => {
display_name: 'Justin L', display_name: 'Justin L',
avatar: 'test.jpg', avatar: 'test.jpg',
verified: true, verified: true,
}); }) as Account;
const expectedActions = [ const expectedActions = [
{ type: 'ACCOUNT_NOTE_INIT_MODAL', account, comment: 'hello' }, { type: 'ACCOUNT_NOTE_INIT_MODAL', account, comment: 'hello' },
{ type: 'MODAL_OPEN', modalType: 'ACCOUNT_NOTE' }, { type: 'MODAL_OPEN', modalType: 'ACCOUNT_NOTE' },
@ -88,10 +90,10 @@ describe('initAccountNoteModal()', () => {
}); });
describe('changeAccountNoteComment()', () => { describe('changeAccountNoteComment()', () => {
let store; let store: ReturnType<typeof mockStore>;
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}); const state = rootState;
store = mockStore(state); store = mockStore(state);
}); });

View File

@ -1,10 +1,10 @@
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { mockStore } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import rootReducer from 'soapbox/reducers'; import { ListRecord, ReducerRecord } from 'soapbox/reducers/user_lists';
import { normalizeAccount } from '../../normalizers'; import { normalizeAccount, normalizeInstance, normalizeRelationship } from '../../normalizers';
import { import {
authorizeFollowRequest, authorizeFollowRequest,
blockAccount, blockAccount,
@ -28,7 +28,7 @@ import {
unsubscribeAccount, unsubscribeAccount,
} from '../accounts'; } from '../accounts';
let store; let store: ReturnType<typeof mockStore>;
describe('createAccount()', () => { describe('createAccount()', () => {
const params = { const params = {
@ -37,7 +37,7 @@ describe('createAccount()', () => {
describe('with a successful API request', () => { describe('with a successful API request', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}); const state = rootState;
store = mockStore(state); store = mockStore(state);
__stub((mock) => { __stub((mock) => {
@ -74,10 +74,10 @@ describe('fetchAccount()', () => {
avatar: 'test.jpg', avatar: 'test.jpg',
}); });
const state = rootReducer(undefined, {}) const state = rootState
.set('accounts', ImmutableMap({ .set('accounts', ImmutableMap({
[id]: account, [id]: account,
})); }) as any);
store = mockStore(state); store = mockStore(state);
@ -98,7 +98,7 @@ describe('fetchAccount()', () => {
const account = require('soapbox/__fixtures__/pleroma-account.json'); const account = require('soapbox/__fixtures__/pleroma-account.json');
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}); const state = rootState;
store = mockStore(state); store = mockStore(state);
__stub((mock) => { __stub((mock) => {
@ -125,7 +125,7 @@ describe('fetchAccount()', () => {
describe('with an unsuccessful API request', () => { describe('with an unsuccessful API request', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}); const state = rootState;
store = mockStore(state); store = mockStore(state);
__stub((mock) => { __stub((mock) => {
@ -155,7 +155,7 @@ describe('fetchAccount()', () => {
describe('fetchAccountByUsername()', () => { describe('fetchAccountByUsername()', () => {
const id = '123'; const id = '123';
const username = 'tiger'; const username = 'tiger';
let state, account; let state, account: any;
beforeEach(() => { beforeEach(() => {
account = normalizeAccount({ account = normalizeAccount({
@ -166,7 +166,7 @@ describe('fetchAccountByUsername()', () => {
birthday: undefined, birthday: undefined,
}); });
state = rootReducer(undefined, {}) state = rootState
.set('accounts', ImmutableMap({ .set('accounts', ImmutableMap({
[id]: account, [id]: account,
})); }));
@ -180,15 +180,15 @@ describe('fetchAccountByUsername()', () => {
describe('when "accountByUsername" feature is enabled', () => { describe('when "accountByUsername" feature is enabled', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('instance', { .set('instance', normalizeInstance({
version: '2.7.2 (compatible; Pleroma 2.4.52-1337-g4779199e.gleasonator+soapbox)', version: '2.7.2 (compatible; Pleroma 2.4.52-1337-g4779199e.gleasonator+soapbox)',
pleroma: ImmutableMap({ pleroma: ImmutableMap({
metadata: ImmutableMap({ metadata: ImmutableMap({
features: [], features: [],
}), }),
}), }),
}) }))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -243,15 +243,15 @@ describe('fetchAccountByUsername()', () => {
describe('when "accountLookup" feature is enabled', () => { describe('when "accountLookup" feature is enabled', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('instance', { .set('instance', normalizeInstance({
version: '3.4.1 (compatible; TruthSocial 1.0.0)', version: '3.4.1 (compatible; TruthSocial 1.0.0)',
pleroma: ImmutableMap({ pleroma: ImmutableMap({
metadata: ImmutableMap({ metadata: ImmutableMap({
features: [], features: [],
}), }),
}), }),
}) }))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -308,7 +308,7 @@ describe('fetchAccountByUsername()', () => {
describe('when using the accountSearch function', () => { describe('when using the accountSearch function', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -373,12 +373,12 @@ describe('fetchAccountByUsername()', () => {
describe('followAccount()', () => { describe('followAccount()', () => {
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
it('should do nothing', async() => { it('should do nothing', async() => {
await store.dispatch(followAccount(1)); await store.dispatch(followAccount('1'));
const actions = store.getActions(); const actions = store.getActions();
expect(actions).toEqual([]); expect(actions).toEqual([]);
@ -386,10 +386,10 @@ describe('followAccount()', () => {
}); });
describe('when logged in', () => { describe('when logged in', () => {
const id = 1; const id = '1';
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -460,12 +460,12 @@ describe('followAccount()', () => {
describe('unfollowAccount()', () => { describe('unfollowAccount()', () => {
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
it('should do nothing', async() => { it('should do nothing', async() => {
await store.dispatch(unfollowAccount(1)); await store.dispatch(unfollowAccount('1'));
const actions = store.getActions(); const actions = store.getActions();
expect(actions).toEqual([]); expect(actions).toEqual([]);
@ -473,10 +473,10 @@ describe('unfollowAccount()', () => {
}); });
describe('when logged in', () => { describe('when logged in', () => {
const id = 1; const id = '1';
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -489,7 +489,7 @@ describe('unfollowAccount()', () => {
it('should dispatch the correct actions', async() => { it('should dispatch the correct actions', async() => {
const expectedActions = [ const expectedActions = [
{ type: 'ACCOUNT_UNFOLLOW_REQUEST', id: 1, skipLoading: true }, { type: 'ACCOUNT_UNFOLLOW_REQUEST', id: '1', skipLoading: true },
{ {
type: 'ACCOUNT_UNFOLLOW_SUCCESS', type: 'ACCOUNT_UNFOLLOW_SUCCESS',
relationship: { success: true }, relationship: { success: true },
@ -534,11 +534,11 @@ describe('unfollowAccount()', () => {
}); });
describe('blockAccount()', () => { describe('blockAccount()', () => {
const id = 1; const id = '1';
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -552,7 +552,7 @@ describe('blockAccount()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -601,11 +601,11 @@ describe('blockAccount()', () => {
}); });
describe('unblockAccount()', () => { describe('unblockAccount()', () => {
const id = 1; const id = '1';
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -619,7 +619,7 @@ describe('unblockAccount()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -667,11 +667,11 @@ describe('unblockAccount()', () => {
}); });
describe('muteAccount()', () => { describe('muteAccount()', () => {
const id = 1; const id = '1';
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -685,7 +685,7 @@ describe('muteAccount()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -734,11 +734,11 @@ describe('muteAccount()', () => {
}); });
describe('unmuteAccount()', () => { describe('unmuteAccount()', () => {
const id = 1; const id = '1';
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -752,7 +752,7 @@ describe('unmuteAccount()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -800,11 +800,11 @@ describe('unmuteAccount()', () => {
}); });
describe('subscribeAccount()', () => { describe('subscribeAccount()', () => {
const id = 1; const id = '1';
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -818,7 +818,7 @@ describe('subscribeAccount()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -866,11 +866,11 @@ describe('subscribeAccount()', () => {
}); });
describe('unsubscribeAccount()', () => { describe('unsubscribeAccount()', () => {
const id = 1; const id = '1';
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -884,7 +884,7 @@ describe('unsubscribeAccount()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -936,7 +936,7 @@ describe('removeFromFollowers()', () => {
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -950,7 +950,7 @@ describe('removeFromFollowers()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -1002,7 +1002,7 @@ describe('fetchFollowers()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -1059,7 +1059,7 @@ describe('expandFollowers()', () => {
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -1073,28 +1073,28 @@ describe('expandFollowers()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('user_lists', { .set('user_lists', ReducerRecord({
followers: ImmutableMap({ followers: ImmutableMap({
[id]: { [id]: ListRecord({
next: 'next_url', next: 'next_url',
},
}), }),
}) }),
}))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
describe('when the url is null', () => { describe('when the url is null', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('user_lists', { .set('user_lists', ReducerRecord({
followers: ImmutableMap({ followers: ImmutableMap({
[id]: { [id]: ListRecord({
next: null, next: null,
},
}), }),
}) }),
}))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -1160,7 +1160,7 @@ describe('fetchFollowing()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -1217,7 +1217,7 @@ describe('expandFollowing()', () => {
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -1231,28 +1231,28 @@ describe('expandFollowing()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('user_lists', { .set('user_lists', ReducerRecord({
following: ImmutableMap({ following: ImmutableMap({
[id]: { [id]: ListRecord({
next: 'next_url', next: 'next_url',
},
}), }),
}) }),
}))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
describe('when the url is null', () => { describe('when the url is null', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('user_lists', { .set('user_lists', ReducerRecord({
following: ImmutableMap({ following: ImmutableMap({
[id]: { [id]: ListRecord({
next: null, next: null,
},
}), }),
}) }),
}))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -1318,7 +1318,7 @@ describe('fetchRelationships()', () => {
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -1332,15 +1332,15 @@ describe('fetchRelationships()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
describe('without newAccountIds', () => { describe('without newAccountIds', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('relationships', ImmutableMap({ [id]: {} })) .set('relationships', ImmutableMap({ [id]: normalizeRelationship({}) }))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -1355,7 +1355,7 @@ describe('fetchRelationships()', () => {
describe('with a successful API request', () => { describe('with a successful API request', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('relationships', ImmutableMap({})) .set('relationships', ImmutableMap({}))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
@ -1409,7 +1409,7 @@ describe('fetchRelationships()', () => {
describe('fetchFollowRequests()', () => { describe('fetchFollowRequests()', () => {
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -1423,14 +1423,14 @@ describe('fetchFollowRequests()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
describe('with a successful API request', () => { describe('with a successful API request', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('relationships', ImmutableMap({})) .set('relationships', ImmutableMap({}))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
@ -1483,7 +1483,7 @@ describe('fetchFollowRequests()', () => {
describe('expandFollowRequests()', () => { describe('expandFollowRequests()', () => {
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -1497,24 +1497,24 @@ describe('expandFollowRequests()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('user_lists', { .set('user_lists', ReducerRecord({
follow_requests: { follow_requests: ListRecord({
next: 'next_url', next: 'next_url',
}, }),
}) }))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
describe('when the url is null', () => { describe('when the url is null', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('user_lists', { .set('user_lists', ReducerRecord({
follow_requests: { follow_requests: ListRecord({
next: null, next: null,
}, }),
}) }))
.set('me', '123'); .set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });
@ -1579,7 +1579,7 @@ describe('authorizeFollowRequest()', () => {
describe('when logged out', () => { describe('when logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -1593,7 +1593,7 @@ describe('authorizeFollowRequest()', () => {
describe('when logged in', () => { describe('when logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '123'); const state = rootState.set('me', '123');
store = mockStore(state); store = mockStore(state);
}); });

View File

@ -1,11 +1,10 @@
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { mockStore } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import rootReducer from 'soapbox/reducers';
import { dismissAlert, showAlert, showAlertForError } from '../alerts'; import { dismissAlert, showAlert, showAlertForError } from '../alerts';
const buildError = (message: string, status: number) => new AxiosError<any>(message, String(status), null, null, { const buildError = (message: string, status: number) => new AxiosError<any>(message, String(status), undefined, null, {
data: { data: {
error: message, error: message,
}, },
@ -15,10 +14,10 @@ const buildError = (message: string, status: number) => new AxiosError<any>(mess
config: {}, config: {},
}); });
let store; let store: ReturnType<typeof mockStore>;
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}); const state = rootState;
store = mockStore(state); store = mockStore(state);
}); });
@ -28,7 +27,7 @@ describe('dismissAlert()', () => {
const expectedActions = [ const expectedActions = [
{ type: 'ALERT_DISMISS', alert }, { type: 'ALERT_DISMISS', alert },
]; ];
await store.dispatch(dismissAlert(alert)); await store.dispatch(dismissAlert(alert as any));
const actions = store.getActions(); const actions = store.getActions();
expect(actions).toEqual(expectedActions); expect(actions).toEqual(expectedActions);
@ -70,11 +69,10 @@ describe('showAlert()', () => {
it('dispatches the proper actions', async() => { it('dispatches the proper actions', async() => {
const error = buildError('', 404); const error = buildError('', 404);
const expectedActions = [];
await store.dispatch(showAlertForError(error)); await store.dispatch(showAlertForError(error));
const actions = store.getActions(); const actions = store.getActions();
expect(actions).toEqual(expectedActions); expect(actions).toEqual([]);
}); });
}); });
@ -82,11 +80,10 @@ describe('showAlert()', () => {
it('dispatches the proper actions', async() => { it('dispatches the proper actions', async() => {
const error = buildError('', 410); const error = buildError('', 410);
const expectedActions = [];
await store.dispatch(showAlertForError(error)); await store.dispatch(showAlertForError(error));
const actions = store.getActions(); const actions = store.getActions();
expect(actions).toEqual(expectedActions); expect(actions).toEqual([]);
}); });
}); });

View File

@ -1,8 +1,6 @@
import { Record as ImmutableRecord } from 'immutable';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { mockStore } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import rootReducer from 'soapbox/reducers'; import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user_lists';
import { expandBlocks, fetchBlocks } from '../blocks'; import { expandBlocks, fetchBlocks } from '../blocks';
@ -14,11 +12,11 @@ const account = {
}; };
describe('fetchBlocks()', () => { describe('fetchBlocks()', () => {
let store; let store: ReturnType<typeof mockStore>;
describe('if logged out', () => { describe('if logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -32,7 +30,7 @@ describe('fetchBlocks()', () => {
describe('if logged in', () => { describe('if logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '1234'); const state = rootState.set('me', '1234');
store = mockStore(state); store = mockStore(state);
}); });
@ -87,11 +85,11 @@ describe('fetchBlocks()', () => {
}); });
describe('expandBlocks()', () => { describe('expandBlocks()', () => {
let store; let store: ReturnType<typeof mockStore>;
describe('if logged out', () => { describe('if logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -105,15 +103,15 @@ describe('expandBlocks()', () => {
describe('if logged in', () => { describe('if logged in', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', '1234'); const state = rootState.set('me', '1234');
store = mockStore(state); store = mockStore(state);
}); });
describe('without a url', () => { describe('without a url', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('me', '1234') .set('me', '1234')
.set('user_lists', ImmutableRecord({ blocks: { next: null } })()); .set('user_lists', UserListsRecord({ blocks: ListRecord({ next: null }) }));
store = mockStore(state); store = mockStore(state);
}); });
@ -127,9 +125,9 @@ describe('expandBlocks()', () => {
describe('with a url', () => { describe('with a url', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('me', '1234') .set('me', '1234')
.set('user_lists', ImmutableRecord({ blocks: { next: 'example' } })()); .set('user_lists', UserListsRecord({ blocks: ListRecord({ next: 'example' }) }));
store = mockStore(state); store = mockStore(state);
}); });

View File

@ -4,14 +4,14 @@ import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { fetchCarouselAvatars } from '../carousels'; import { fetchCarouselAvatars } from '../carousels';
describe('fetchCarouselAvatars()', () => { describe('fetchCarouselAvatars()', () => {
let store; let store: ReturnType<typeof mockStore>;
beforeEach(() => { beforeEach(() => {
store = mockStore(rootState); store = mockStore(rootState);
}); });
describe('with a successful API request', () => { describe('with a successful API request', () => {
let avatars; let avatars: Record<string, any>[];
beforeEach(() => { beforeEach(() => {
avatars = [ avatars = [

View File

@ -1,28 +1,29 @@
import { fromJS } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { mockStore } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { InstanceRecord } from 'soapbox/normalizers'; import { InstanceRecord } from 'soapbox/normalizers';
import rootReducer from 'soapbox/reducers';
import { uploadCompose } from '../compose'; import { uploadCompose } from '../compose';
import type { IntlShape } from 'react-intl';
describe('uploadCompose()', () => { describe('uploadCompose()', () => {
describe('with images', () => { describe('with images', () => {
let files, store; let files: FileList, store: ReturnType<typeof mockStore>;
beforeEach(() => { beforeEach(() => {
const instance = InstanceRecord({ const instance = InstanceRecord({
configuration: fromJS({ configuration: ImmutableMap({
statuses: { statuses: ImmutableMap({
max_media_attachments: 4, max_media_attachments: 4,
}, }),
media_attachments: { media_attachments: ImmutableMap({
image_size_limit: 10, image_size_limit: 10,
}, }),
}), }),
}); });
const state = rootReducer(undefined, {}) const state = rootState
.set('me', '1234') .set('me', '1234')
.set('instance', instance); .set('instance', instance);
@ -32,13 +33,13 @@ describe('uploadCompose()', () => {
name: 'Image', name: 'Image',
size: 15, size: 15,
type: 'image/png', type: 'image/png',
}]; }] as unknown as FileList;
}); });
it('creates an alert if exceeds max size', async() => { it('creates an alert if exceeds max size', async() => {
const mockIntl = { const mockIntl = {
formatMessage: jest.fn().mockReturnValue('Image exceeds the current file size limit (10 Bytes)'), formatMessage: jest.fn().mockReturnValue('Image exceeds the current file size limit (10 Bytes)'),
}; } as unknown as IntlShape;
const expectedActions = [ const expectedActions = [
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true }, { type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
@ -60,21 +61,21 @@ describe('uploadCompose()', () => {
}); });
describe('with videos', () => { describe('with videos', () => {
let files, store; let files: FileList, store: ReturnType<typeof mockStore>;
beforeEach(() => { beforeEach(() => {
const instance = InstanceRecord({ const instance = InstanceRecord({
configuration: fromJS({ configuration: ImmutableMap({
statuses: { statuses: ImmutableMap({
max_media_attachments: 4, max_media_attachments: 4,
}, }),
media_attachments: { media_attachments: ImmutableMap({
video_size_limit: 10, video_size_limit: 10,
}, }),
}), }),
}); });
const state = rootReducer(undefined, {}) const state = rootState
.set('me', '1234') .set('me', '1234')
.set('instance', instance); .set('instance', instance);
@ -84,13 +85,13 @@ describe('uploadCompose()', () => {
name: 'Video', name: 'Video',
size: 15, size: 15,
type: 'video/mp4', type: 'video/mp4',
}]; }] as unknown as FileList;
}); });
it('creates an alert if exceeds max size', async() => { it('creates an alert if exceeds max size', async() => {
const mockIntl = { const mockIntl = {
formatMessage: jest.fn().mockReturnValue('Video exceeds the current file size limit (10 Bytes)'), formatMessage: jest.fn().mockReturnValue('Video exceeds the current file size limit (10 Bytes)'),
}; } as unknown as IntlShape;
const expectedActions = [ const expectedActions = [
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true }, { type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },

View File

@ -1,5 +1,4 @@
import { mockStore, mockWindowProperty } from 'soapbox/jest/test-helpers'; import { mockStore, mockWindowProperty, rootState } from 'soapbox/jest/test-helpers';
import rootReducer from 'soapbox/reducers';
import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding'; import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding';
@ -17,7 +16,7 @@ describe('checkOnboarding()', () => {
it('does nothing if localStorage item is not set', async() => { it('does nothing if localStorage item is not set', async() => {
mockGetItem = jest.fn().mockReturnValue(null); mockGetItem = jest.fn().mockReturnValue(null);
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } }); const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const store = mockStore(state); const store = mockStore(state);
await store.dispatch(checkOnboardingStatus()); await store.dispatch(checkOnboardingStatus());
@ -30,7 +29,7 @@ describe('checkOnboarding()', () => {
it('does nothing if localStorage item is invalid', async() => { it('does nothing if localStorage item is invalid', async() => {
mockGetItem = jest.fn().mockReturnValue('invalid'); mockGetItem = jest.fn().mockReturnValue('invalid');
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } }); const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const store = mockStore(state); const store = mockStore(state);
await store.dispatch(checkOnboardingStatus()); await store.dispatch(checkOnboardingStatus());
@ -43,7 +42,7 @@ describe('checkOnboarding()', () => {
it('dispatches the correct action', async() => { it('dispatches the correct action', async() => {
mockGetItem = jest.fn().mockReturnValue('1'); mockGetItem = jest.fn().mockReturnValue('1');
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } }); const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const store = mockStore(state); const store = mockStore(state);
await store.dispatch(checkOnboardingStatus()); await store.dispatch(checkOnboardingStatus());
@ -66,7 +65,7 @@ describe('startOnboarding()', () => {
}); });
it('dispatches the correct action', async() => { it('dispatches the correct action', async() => {
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } }); const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const store = mockStore(state); const store = mockStore(state);
await store.dispatch(startOnboarding()); await store.dispatch(startOnboarding());
@ -89,7 +88,7 @@ describe('endOnboarding()', () => {
}); });
it('dispatches the correct action', async() => { it('dispatches the correct action', async() => {
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } }); const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const store = mockStore(state); const store = mockStore(state);
await store.dispatch(endOnboarding()); await store.dispatch(endOnboarding());

View File

@ -4,7 +4,6 @@ import { STATUSES_IMPORT } from 'soapbox/actions/importer';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { normalizeStatus } from 'soapbox/normalizers/status'; import { normalizeStatus } from 'soapbox/normalizers/status';
import rootReducer from 'soapbox/reducers';
import { deleteStatus, fetchContext } from '../statuses'; import { deleteStatus, fetchContext } from '../statuses';
@ -19,7 +18,7 @@ describe('fetchContext()', () => {
const store = mockStore(rootState); const store = mockStore(rootState);
store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70')).then(context => { store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70')).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[3].type).toEqual(STATUSES_IMPORT); expect(actions[3].type).toEqual(STATUSES_IMPORT);
@ -31,11 +30,11 @@ describe('fetchContext()', () => {
}); });
describe('deleteStatus()', () => { describe('deleteStatus()', () => {
let store; let store: ReturnType<typeof mockStore>;
describe('if logged out', () => { describe('if logged out', () => {
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}).set('me', null); const state = rootState.set('me', null);
store = mockStore(state); store = mockStore(state);
}); });
@ -54,16 +53,16 @@ describe('deleteStatus()', () => {
}); });
beforeEach(() => { beforeEach(() => {
const state = rootReducer(undefined, {}) const state = rootState
.set('me', '1234') .set('me', '1234')
.set('statuses', fromJS({ .set('statuses', fromJS({
[statusId]: cachedStatus, [statusId]: cachedStatus,
})); }) as any);
store = mockStore(state); store = mockStore(state);
}); });
describe('with a successful API request', () => { describe('with a successful API request', () => {
let status; let status: any;
beforeEach(() => { beforeEach(() => {
status = require('soapbox/__fixtures__/pleroma-status-deleted.json'); status = require('soapbox/__fixtures__/pleroma-status-deleted.json');

View File

@ -0,0 +1,108 @@
import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { normalizeInstance } from 'soapbox/normalizers';
import {
fetchSuggestions,
} from '../suggestions';
let store: ReturnType<typeof mockStore>;
let state;
describe('fetchSuggestions()', () => {
describe('with Truth Social software', () => {
beforeEach(() => {
state = rootState
.set('instance', normalizeInstance({
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
pleroma: ImmutableMap({
metadata: ImmutableMap({
features: [],
}),
}),
}))
.set('me', '123');
store = mockStore(state);
});
describe('with a successful API request', () => {
const response = [
{
account_id: '1',
acct: 'jl',
account_avatar: 'https://example.com/some.jpg',
display_name: 'justin',
note: '<p>note</p>',
verified: true,
},
];
beforeEach(() => {
__stub((mock) => {
mock.onGet('/api/v1/truth/carousels/suggestions').reply(200, response, {
link: '<https://example.com/api/v1/truth/carousels/suggestions?since_id=1>; rel=\'prev\'',
});
});
});
it('dispatches the correct actions', async() => {
const expectedActions = [
{ type: 'SUGGESTIONS_V2_FETCH_REQUEST', skipLoading: true },
{
type: 'ACCOUNTS_IMPORT', accounts: [{
acct: response[0].acct,
avatar: response[0].account_avatar,
avatar_static: response[0].account_avatar,
id: response[0].account_id,
note: response[0].note,
verified: response[0].verified,
display_name: response[0].display_name,
}],
},
{
type: 'SUGGESTIONS_TRUTH_FETCH_SUCCESS',
suggestions: response,
next: undefined,
skipLoading: true,
},
{
type: 'RELATIONSHIPS_FETCH_REQUEST',
skipLoading: true,
ids: [response[0].account_id],
},
];
await store.dispatch(fetchSuggestions());
const actions = store.getActions();
expect(actions).toEqual(expectedActions);
});
});
describe('with an unsuccessful API request', () => {
beforeEach(() => {
__stub((mock) => {
mock.onGet('/api/v1/truth/carousels/suggestions').networkError();
});
});
it('should dispatch the correct actions', async() => {
const expectedActions = [
{ type: 'SUGGESTIONS_V2_FETCH_REQUEST', skipLoading: true },
{
type: 'SUGGESTIONS_V2_FETCH_FAIL',
error: new Error('Network Error'),
skipLoading: true,
skipAlert: true,
},
];
await store.dispatch(fetchSuggestions());
const actions = store.getActions();
expect(actions).toEqual(expectedActions);
});
});
});
});

View File

@ -180,8 +180,8 @@ export const verifyCredentials = (token: string, accountUrl?: string) => {
return account; return account;
} else { } else {
if (getState().me === null) dispatch(fetchMeFail(error)); if (getState().me === null) dispatch(fetchMeFail(error));
dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error, skipAlert: true }); dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error });
return error; throw error;
} }
}); });
}; };
@ -214,14 +214,6 @@ export const logIn = (username: string, password: string) =>
if ((error.response?.data as any).error === 'mfa_required') { if ((error.response?.data as any).error === 'mfa_required') {
// If MFA is required, throw the error and handle it in the component. // If MFA is required, throw the error and handle it in the component.
throw error; throw error;
} else if ((error.response?.data as any).error === 'invalid_grant') {
// Mastodon returns this user-unfriendly error as a catch-all
// for everything from "bad request" to "wrong password".
// Assume our code is correct and it's a wrong password.
dispatch(snackbar.error(messages.invalidCredentials));
} else if ((error.response?.data as any).error) {
// If the backend returns an error, display it.
dispatch(snackbar.error((error.response?.data as any).error));
} else { } else {
// Return "wrong password" message. // Return "wrong password" message.
dispatch(snackbar.error(messages.invalidCredentials)); dispatch(snackbar.error(messages.invalidCredentials));

View File

@ -6,7 +6,7 @@ import api from '../api';
import { loadCredentials } from './auth'; import { loadCredentials } from './auth';
import { importFetchedAccount } from './importer'; import { importFetchedAccount } from './importer';
import type { AxiosError } from 'axios'; import type { AxiosError, AxiosRequestHeaders } from 'axios';
import type { AppDispatch, RootState } from 'soapbox/store'; import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity } from 'soapbox/types/entities'; import type { APIEntity } from 'soapbox/types/entities';
@ -62,12 +62,16 @@ const persistAuthAccount = (account: APIEntity, params: Record<string, any>) =>
} }
}; };
const patchMe = (params: Record<string, any>) => const patchMe = (params: Record<string, any>, isFormData = false) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
dispatch(patchMeRequest()); dispatch(patchMeRequest());
const headers: AxiosRequestHeaders = isFormData ? {
'Content-Type': 'multipart/form-data',
} : {};
return api(getState) return api(getState)
.patch('/api/v1/accounts/update_credentials', params) .patch('/api/v1/accounts/update_credentials', params, { headers })
.then(response => { .then(response => {
persistAuthAccount(response.data, params); persistAuthAccount(response.data, params);
dispatch(patchMeSuccess(response.data)); dispatch(patchMeSuccess(response.data));

View File

@ -44,7 +44,7 @@ const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm =
const name = state.accounts.get(accountId)!.username; const name = state.accounts.get(accountId)!.username;
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/user-off.svg'), icon: require('@tabler/icons/user-off.svg'),
heading: intl.formatMessage(messages.deactivateUserHeading, { acct }), heading: intl.formatMessage(messages.deactivateUserHeading, { acct }),
message: intl.formatMessage(messages.deactivateUserPrompt, { acct }), message: intl.formatMessage(messages.deactivateUserPrompt, { acct }),
confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }), confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }),
@ -83,7 +83,7 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
const checkbox = local ? intl.formatMessage(messages.deleteLocalUserCheckbox) : false; const checkbox = local ? intl.formatMessage(messages.deleteLocalUserCheckbox) : false;
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/user-minus.svg'), icon: require('@tabler/icons/user-minus.svg'),
heading: intl.formatMessage(messages.deleteUserHeading, { acct }), heading: intl.formatMessage(messages.deleteUserHeading, { acct }),
message, message,
confirm, confirm,
@ -106,7 +106,7 @@ const rejectUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
const name = state.accounts.get(accountId)!.username; const name = state.accounts.get(accountId)!.username;
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/user-off.svg'), icon: require('@tabler/icons/user-off.svg'),
heading: intl.formatMessage(messages.rejectUserHeading, { acct }), heading: intl.formatMessage(messages.rejectUserHeading, { acct }),
message: intl.formatMessage(messages.rejectUserPrompt, { acct }), message: intl.formatMessage(messages.rejectUserPrompt, { acct }),
confirm: intl.formatMessage(messages.rejectUserConfirm, { name }), confirm: intl.formatMessage(messages.rejectUserConfirm, { name }),
@ -127,7 +127,7 @@ const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensiti
const acct = state.accounts.get(accountId)!.acct; const acct = state.accounts.get(accountId)!.acct;
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/alert-triangle.svg'), icon: require('@tabler/icons/alert-triangle.svg'),
heading: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading), heading: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading),
message: intl.formatMessage(sensitive === false ? messages.markStatusSensitivePrompt : messages.markStatusNotSensitivePrompt, { acct }), message: intl.formatMessage(sensitive === false ? messages.markStatusSensitivePrompt : messages.markStatusNotSensitivePrompt, { acct }),
confirm: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveConfirm : messages.markStatusNotSensitiveConfirm), confirm: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveConfirm : messages.markStatusNotSensitiveConfirm),
@ -148,7 +148,7 @@ const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = ()
const acct = state.accounts.get(accountId)!.acct; const acct = state.accounts.get(accountId)!.acct;
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/trash.svg'), icon: require('@tabler/icons/trash.svg'),
heading: intl.formatMessage(messages.deleteStatusHeading), heading: intl.formatMessage(messages.deleteStatusHeading),
message: intl.formatMessage(messages.deleteStatusPrompt, { acct }), message: intl.formatMessage(messages.deleteStatusPrompt, { acct }),
confirm: intl.formatMessage(messages.deleteStatusConfirm), confirm: intl.formatMessage(messages.deleteStatusConfirm),

View File

@ -89,6 +89,7 @@ const updateNotifications = (notification: APIEntity) =>
const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record<string, string>, intlLocale: string, curPath: string) => const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record<string, string>, intlLocale: string, curPath: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (!notification.type) return; // drop invalid notifications
if (notification.type === 'pleroma:chat_mention') return; // Drop chat notifications, handle them per-chat if (notification.type === 'pleroma:chat_mention') return; // Drop chat notifications, handle them per-chat
const showAlert = getSettings(getState()).getIn(['notifications', 'alerts', notification.type]); const showAlert = getSettings(getState()).getIn(['notifications', 'alerts', notification.type]);

View File

@ -1,3 +1,5 @@
import { AxiosResponse } from 'axios';
import { isLoggedIn } from 'soapbox/utils/auth'; import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features'; import { getFeatures } from 'soapbox/utils/features';
@ -5,6 +7,7 @@ import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts'; import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts } from './importer';
import { insertSuggestionsIntoTimeline } from './timelines';
import type { AppDispatch, RootState } from 'soapbox/store'; import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity } from 'soapbox/types/entities'; import type { APIEntity } from 'soapbox/types/entities';
@ -19,6 +22,10 @@ const SUGGESTIONS_V2_FETCH_REQUEST = 'SUGGESTIONS_V2_FETCH_REQUEST';
const SUGGESTIONS_V2_FETCH_SUCCESS = 'SUGGESTIONS_V2_FETCH_SUCCESS'; const SUGGESTIONS_V2_FETCH_SUCCESS = 'SUGGESTIONS_V2_FETCH_SUCCESS';
const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL'; const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL';
const SUGGESTIONS_TRUTH_FETCH_REQUEST = 'SUGGESTIONS_TRUTH_FETCH_REQUEST';
const SUGGESTIONS_TRUTH_FETCH_SUCCESS = 'SUGGESTIONS_TRUTH_FETCH_SUCCESS';
const SUGGESTIONS_TRUTH_FETCH_FAIL = 'SUGGESTIONS_TRUTH_FETCH_FAIL';
const fetchSuggestionsV1 = (params: Record<string, any> = {}) => const fetchSuggestionsV1 = (params: Record<string, any> = {}) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: SUGGESTIONS_FETCH_REQUEST, skipLoading: true }); dispatch({ type: SUGGESTIONS_FETCH_REQUEST, skipLoading: true });
@ -52,6 +59,48 @@ const fetchSuggestionsV2 = (params: Record<string, any> = {}) =>
}); });
}; };
export type SuggestedProfile = {
account_avatar: string
account_id: string
acct: string
display_name: string
note: string
verified: boolean
}
const mapSuggestedProfileToAccount = (suggestedProfile: SuggestedProfile) => ({
id: suggestedProfile.account_id,
avatar: suggestedProfile.account_avatar,
avatar_static: suggestedProfile.account_avatar,
acct: suggestedProfile.acct,
display_name: suggestedProfile.display_name,
note: suggestedProfile.note,
verified: suggestedProfile.verified,
});
const fetchTruthSuggestions = (params: Record<string, any> = {}) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const next = getState().suggestions.next;
dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true });
return api(getState)
.get(next ? next : '/api/v1/truth/carousels/suggestions', next ? {} : { params })
.then((response: AxiosResponse<SuggestedProfile[]>) => {
const suggestedProfiles = response.data;
const next = getLinks(response).refs.find(link => link.rel === 'next')?.uri;
const accounts = suggestedProfiles.map(mapSuggestedProfileToAccount);
dispatch(importFetchedAccounts(accounts));
dispatch({ type: SUGGESTIONS_TRUTH_FETCH_SUCCESS, suggestions: suggestedProfiles, next, skipLoading: true });
return suggestedProfiles;
})
.catch(error => {
dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true });
throw error;
});
};
const fetchSuggestions = (params: Record<string, any> = { limit: 50 }) => const fetchSuggestions = (params: Record<string, any> = { limit: 50 }) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
@ -59,17 +108,24 @@ const fetchSuggestions = (params: Record<string, any> = { limit: 50 }) =>
const instance = state.instance; const instance = state.instance;
const features = getFeatures(instance); const features = getFeatures(instance);
if (!me) return; if (!me) return null;
if (features.suggestionsV2) { if (features.truthSuggestions) {
dispatch(fetchSuggestionsV2(params)) return dispatch(fetchTruthSuggestions(params))
.then((suggestions: APIEntity[]) => {
const accountIds = suggestions.map((account) => account.account_id);
dispatch(fetchRelationships(accountIds));
})
.catch(() => { });
} else if (features.suggestionsV2) {
return dispatch(fetchSuggestionsV2(params))
.then((suggestions: APIEntity[]) => { .then((suggestions: APIEntity[]) => {
const accountIds = suggestions.map(({ account }) => account.id); const accountIds = suggestions.map(({ account }) => account.id);
dispatch(fetchRelationships(accountIds)); dispatch(fetchRelationships(accountIds));
}) })
.catch(() => { }); .catch(() => { });
} else if (features.suggestions) { } else if (features.suggestions) {
dispatch(fetchSuggestionsV1(params)) return dispatch(fetchSuggestionsV1(params))
.then((accounts: APIEntity[]) => { .then((accounts: APIEntity[]) => {
const accountIds = accounts.map(({ id }) => id); const accountIds = accounts.map(({ id }) => id);
dispatch(fetchRelationships(accountIds)); dispatch(fetchRelationships(accountIds));
@ -77,9 +133,14 @@ const fetchSuggestions = (params: Record<string, any> = { limit: 50 }) =>
.catch(() => { }); .catch(() => { });
} else { } else {
// Do nothing // Do nothing
return null;
} }
}; };
const fetchSuggestionsForTimeline = () => (dispatch: AppDispatch, _getState: () => RootState) => {
dispatch(fetchSuggestions({ limit: 20 }))?.then(() => dispatch(insertSuggestionsIntoTimeline()));
};
const dismissSuggestion = (accountId: string) => const dismissSuggestion = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return; if (!isLoggedIn(getState)) return;
@ -100,8 +161,12 @@ export {
SUGGESTIONS_V2_FETCH_REQUEST, SUGGESTIONS_V2_FETCH_REQUEST,
SUGGESTIONS_V2_FETCH_SUCCESS, SUGGESTIONS_V2_FETCH_SUCCESS,
SUGGESTIONS_V2_FETCH_FAIL, SUGGESTIONS_V2_FETCH_FAIL,
SUGGESTIONS_TRUTH_FETCH_REQUEST,
SUGGESTIONS_TRUTH_FETCH_SUCCESS,
SUGGESTIONS_TRUTH_FETCH_FAIL,
fetchSuggestionsV1, fetchSuggestionsV1,
fetchSuggestionsV2, fetchSuggestionsV2,
fetchSuggestions, fetchSuggestions,
fetchSuggestionsForTimeline,
dismissSuggestion, dismissSuggestion,
}; };

View File

@ -27,6 +27,7 @@ const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
const TIMELINE_REPLACE = 'TIMELINE_REPLACE'; const TIMELINE_REPLACE = 'TIMELINE_REPLACE';
const TIMELINE_INSERT = 'TIMELINE_INSERT';
const MAX_QUEUED_ITEMS = 40; const MAX_QUEUED_ITEMS = 40;
@ -127,7 +128,7 @@ const clearTimeline = (timeline: string) =>
(dispatch: AppDispatch) => (dispatch: AppDispatch) =>
dispatch({ type: TIMELINE_CLEAR, timeline }); dispatch({ type: TIMELINE_CLEAR, timeline });
const noOp = () => {}; const noOp = () => { };
const noOpAsync = () => () => new Promise(f => f(undefined)); const noOpAsync = () => () => new Promise(f => f(undefined));
const parseTags = (tags: Record<string, any[]> = {}, mode: 'any' | 'all' | 'none') => { const parseTags = (tags: Record<string, any[]> = {}, mode: 'any' | 'all' | 'none') => {
@ -141,7 +142,7 @@ const replaceHomeTimeline = (
{ maxId }: Record<string, any> = {}, { maxId }: Record<string, any> = {},
) => (dispatch: AppDispatch, _getState: () => RootState) => { ) => (dispatch: AppDispatch, _getState: () => RootState) => {
dispatch({ type: TIMELINE_REPLACE, accountId }); dispatch({ type: TIMELINE_REPLACE, accountId });
dispatch(expandHomeTimeline({ accountId, maxId })); dispatch(expandHomeTimeline({ accountId, maxId }, () => dispatch(insertSuggestionsIntoTimeline())));
}; };
const expandTimeline = (timelineId: string, path: string, params: Record<string, any> = {}, done = noOp) => const expandTimeline = (timelineId: string, path: string, params: Record<string, any> = {}, done = noOp) =>
@ -259,6 +260,10 @@ const scrollTopTimeline = (timeline: string, top: boolean) => ({
top, top,
}); });
const insertSuggestionsIntoTimeline = () => (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: TIMELINE_INSERT, timeline: 'home' });
};
export { export {
TIMELINE_UPDATE, TIMELINE_UPDATE,
TIMELINE_DELETE, TIMELINE_DELETE,
@ -272,6 +277,7 @@ export {
TIMELINE_CONNECT, TIMELINE_CONNECT,
TIMELINE_DISCONNECT, TIMELINE_DISCONNECT,
TIMELINE_REPLACE, TIMELINE_REPLACE,
TIMELINE_INSERT,
MAX_QUEUED_ITEMS, MAX_QUEUED_ITEMS,
processTimelineUpdate, processTimelineUpdate,
updateTimeline, updateTimeline,
@ -298,4 +304,5 @@ export {
connectTimeline, connectTimeline,
disconnectTimeline, disconnectTimeline,
scrollTopTimeline, scrollTopTimeline,
insertSuggestionsIntoTimeline,
}; };

View File

@ -5,6 +5,8 @@ import { render, screen } from '../../jest/test-helpers';
import { normalizeAccount } from '../../normalizers'; import { normalizeAccount } from '../../normalizers';
import Account from '../account'; import Account from '../account';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
describe('<Account />', () => { describe('<Account />', () => {
it('renders account name and username', () => { it('renders account name and username', () => {
const account = normalizeAccount({ const account = normalizeAccount({
@ -12,7 +14,7 @@ describe('<Account />', () => {
acct: 'justin-username', acct: 'justin-username',
display_name: 'Justin L', display_name: 'Justin L',
avatar: 'test.jpg', avatar: 'test.jpg',
}); }) as ReducerAccount;
const store = { const store = {
accounts: ImmutableMap({ accounts: ImmutableMap({
@ -20,7 +22,7 @@ describe('<Account />', () => {
}), }),
}; };
render(<Account account={account} />, null, store); render(<Account account={account} />, undefined, store);
expect(screen.getByTestId('account')).toHaveTextContent('Justin L'); expect(screen.getByTestId('account')).toHaveTextContent('Justin L');
expect(screen.getByTestId('account')).toHaveTextContent(/justin-username/i); expect(screen.getByTestId('account')).toHaveTextContent(/justin-username/i);
}); });
@ -33,7 +35,7 @@ describe('<Account />', () => {
display_name: 'Justin L', display_name: 'Justin L',
avatar: 'test.jpg', avatar: 'test.jpg',
verified: true, verified: true,
}); }) as ReducerAccount;
const store = { const store = {
accounts: ImmutableMap({ accounts: ImmutableMap({
@ -41,7 +43,7 @@ describe('<Account />', () => {
}), }),
}; };
render(<Account account={account} />, null, store); render(<Account account={account} />, undefined, store);
expect(screen.getByTestId('verified-badge')).toBeInTheDocument(); expect(screen.getByTestId('verified-badge')).toBeInTheDocument();
}); });
@ -52,7 +54,7 @@ describe('<Account />', () => {
display_name: 'Justin L', display_name: 'Justin L',
avatar: 'test.jpg', avatar: 'test.jpg',
verified: false, verified: false,
}); }) as ReducerAccount;
const store = { const store = {
accounts: ImmutableMap({ accounts: ImmutableMap({
@ -60,7 +62,7 @@ describe('<Account />', () => {
}), }),
}; };
render(<Account account={account} />, null, store); render(<Account account={account} />, undefined, store);
expect(screen.queryAllByTestId('verified-badge')).toHaveLength(0); expect(screen.queryAllByTestId('verified-badge')).toHaveLength(0);
}); });
}); });

View File

@ -5,6 +5,8 @@ import { normalizeAccount } from 'soapbox/normalizers';
import { render, screen } from '../../jest/test-helpers'; import { render, screen } from '../../jest/test-helpers';
import Avatar from '../avatar'; import Avatar from '../avatar';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
describe('<Avatar />', () => { describe('<Avatar />', () => {
const account = normalizeAccount({ const account = normalizeAccount({
username: 'alice', username: 'alice',
@ -12,7 +14,7 @@ describe('<Avatar />', () => {
display_name: 'Alice', display_name: 'Alice',
avatar: '/animated/alice.gif', avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg', avatar_static: '/static/alice.jpg',
}); }) as ReducerAccount;
const size = 100; const size = 100;

View File

@ -1,25 +1,28 @@
import { fromJS } from 'immutable';
import React from 'react'; import React from 'react';
import { normalizeAccount } from 'soapbox/normalizers';
import { render, screen } from '../../jest/test-helpers'; import { render, screen } from '../../jest/test-helpers';
import AvatarOverlay from '../avatar_overlay'; import AvatarOverlay from '../avatar_overlay';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
describe('<AvatarOverlay', () => { describe('<AvatarOverlay', () => {
const account = fromJS({ const account = normalizeAccount({
username: 'alice', username: 'alice',
acct: 'alice', acct: 'alice',
display_name: 'Alice', display_name: 'Alice',
avatar: '/animated/alice.gif', avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg', avatar_static: '/static/alice.jpg',
}); }) as ReducerAccount;
const friend = fromJS({ const friend = normalizeAccount({
username: 'eve', username: 'eve',
acct: 'eve@blackhat.lair', acct: 'eve@blackhat.lair',
display_name: 'Evelyn', display_name: 'Evelyn',
avatar: '/animated/eve.gif', avatar: '/animated/eve.gif',
avatar_static: '/static/eve.jpg', avatar_static: '/static/eve.jpg',
}); }) as ReducerAccount;
it('renders a overlay avatar', () => { it('renders a overlay avatar', () => {
render(<AvatarOverlay account={account} friend={friend} />); render(<AvatarOverlay account={account} friend={friend} />);

View File

@ -5,9 +5,11 @@ import { normalizeAccount } from 'soapbox/normalizers';
import { render, screen } from '../../jest/test-helpers'; import { render, screen } from '../../jest/test-helpers';
import DisplayName from '../display-name'; import DisplayName from '../display-name';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
describe('<DisplayName />', () => { describe('<DisplayName />', () => {
it('renders display name + account name', () => { it('renders display name + account name', () => {
const account = normalizeAccount({ acct: 'bar@baz' }); const account = normalizeAccount({ acct: 'bar@baz' }) as ReducerAccount;
render(<DisplayName account={account} />); render(<DisplayName account={account} />);
expect(screen.getByTestId('display-name')).toHaveTextContent('bar@baz'); expect(screen.getByTestId('display-name')).toHaveTextContent('bar@baz');

View File

@ -6,6 +6,7 @@ import EmojiSelector from '../emoji_selector';
describe('<EmojiSelector />', () => { describe('<EmojiSelector />', () => {
it('renders correctly', () => { it('renders correctly', () => {
const children = <EmojiSelector />; const children = <EmojiSelector />;
// @ts-ignore
children.__proto__.addEventListener = () => {}; children.__proto__.addEventListener = () => {};
render(children); render(children);

View File

@ -4,6 +4,8 @@ import { render, screen, rootState } from '../../jest/test-helpers';
import { normalizeStatus, normalizeAccount } from '../../normalizers'; import { normalizeStatus, normalizeAccount } from '../../normalizers';
import QuotedStatus from '../quoted-status'; import QuotedStatus from '../quoted-status';
import type { ReducerStatus } from 'soapbox/reducers/statuses';
describe('<QuotedStatus />', () => { describe('<QuotedStatus />', () => {
it('renders content', () => { it('renders content', () => {
const account = normalizeAccount({ const account = normalizeAccount({
@ -16,11 +18,11 @@ describe('<QuotedStatus />', () => {
account, account,
content: 'hello world', content: 'hello world',
contentHtml: 'hello world', contentHtml: 'hello world',
}); }) as ReducerStatus;
const state = rootState.setIn(['accounts', '1', account]); const state = rootState.setIn(['accounts', '1'], account);
render(<QuotedStatus status={status} />, null, state); render(<QuotedStatus status={status} />, undefined, state);
screen.getByText(/hello world/i); screen.getByText(/hello world/i);
expect(screen.getByTestId('quoted-status')).toHaveTextContent(/hello world/i); expect(screen.getByTestId('quoted-status')).toHaveTextContent(/hello world/i);
}); });

View File

@ -28,7 +28,7 @@ describe('<ScrollTopButton />', () => {
message={messages.queue} message={messages.queue}
/>, />,
); );
expect(screen.getByText('Click to see 1 new post', { hidden: true })).toBeInTheDocument(); expect(screen.getByText('Click to see 1 new post')).toBeInTheDocument();
render( render(
<ScrollTopButton <ScrollTopButton
@ -38,6 +38,6 @@ describe('<ScrollTopButton />', () => {
message={messages.queue} message={messages.queue}
/>, />,
); );
expect(screen.getByText('Click to see 9999999 new posts', { hidden: true })).toBeInTheDocument(); expect(screen.getByText('Click to see 9999999 new posts')).toBeInTheDocument();
}); });
}); });

View File

@ -9,7 +9,7 @@ import { getAcct } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state'; import { displayFqn } from 'soapbox/utils/state';
import RelativeTimestamp from './relative_timestamp'; import RelativeTimestamp from './relative_timestamp';
import { Avatar, Emoji, HStack, Icon, IconButton, Text } from './ui'; import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
import type { Account as AccountEntity } from 'soapbox/types/entities'; import type { Account as AccountEntity } from 'soapbox/types/entities';
@ -57,7 +57,9 @@ interface IAccount {
timestamp?: string | Date, timestamp?: string | Date,
timestampUrl?: string, timestampUrl?: string,
futureTimestamp?: boolean, futureTimestamp?: boolean,
withAccountNote?: boolean,
withDate?: boolean, withDate?: boolean,
withLinkToProfile?: boolean,
withRelationship?: boolean, withRelationship?: boolean,
showEdit?: boolean, showEdit?: boolean,
emoji?: string, emoji?: string,
@ -78,7 +80,9 @@ const Account = ({
timestamp, timestamp,
timestampUrl, timestampUrl,
futureTimestamp = false, futureTimestamp = false,
withAccountNote = false,
withDate = false, withDate = false,
withLinkToProfile = true,
withRelationship = true, withRelationship = true,
showEdit = false, showEdit = false,
emoji, emoji,
@ -154,12 +158,12 @@ const Account = ({
if (withDate) timestamp = account.created_at; if (withDate) timestamp = account.created_at;
const LinkEl: any = showProfileHoverCard ? Link : 'div'; const LinkEl: any = withLinkToProfile ? Link : 'div';
return ( return (
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}> <div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
<HStack alignItems={actionAlignment} justifyContent='between'> <HStack alignItems={actionAlignment} justifyContent='between'>
<HStack alignItems='center' space={3} grow> <HStack alignItems={withAccountNote ? 'top' : 'center'} space={3}>
<ProfilePopper <ProfilePopper
condition={showProfileHoverCard} condition={showProfileHoverCard}
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>} wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
@ -202,6 +206,7 @@ const Account = ({
</LinkEl> </LinkEl>
</ProfilePopper> </ProfilePopper>
<Stack space={withAccountNote ? 1 : 0}>
<HStack alignItems='center' space={1} style={style}> <HStack alignItems='center' space={1} style={style}>
<Text theme='muted' size='sm' truncate>@{username}</Text> <Text theme='muted' size='sm' truncate>@{username}</Text>
@ -227,10 +232,19 @@ const Account = ({
<> <>
<Text tag='span' theme='muted' size='sm'>&middot;</Text> <Text tag='span' theme='muted' size='sm'>&middot;</Text>
<Icon className='h-5 w-5 stroke-[1.35]' src={require('@tabler/icons/icons/pencil.svg')} /> <Icon className='h-5 w-5 stroke-[1.35]' src={require('@tabler/icons/pencil.svg')} />
</> </>
) : null} ) : null}
</HStack> </HStack>
{withAccountNote && (
<Text
size='sm'
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
className='mr-2'
/>
)}
</Stack>
</div> </div>
</HStack> </HStack>

View File

@ -70,8 +70,8 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
/> />
</label> </label>
<div role='button' tabIndex={0} className='search__icon' onClick={handleClear}> <div role='button' tabIndex={0} className='search__icon' onClick={handleClear}>
<Icon src={require('@tabler/icons/icons/search.svg')} className={classNames('svg-icon--search', { active: isEmpty() })} /> <Icon src={require('@tabler/icons/search.svg')} className={classNames('svg-icon--search', { active: isEmpty() })} />
<Icon src={require('@tabler/icons/icons/backspace.svg')} className={classNames('svg-icon--backspace', { active: !isEmpty() })} aria-label={intl.formatMessage(messages.placeholder)} /> <Icon src={require('@tabler/icons/backspace.svg')} className={classNames('svg-icon--backspace', { active: !isEmpty() })} aria-label={intl.formatMessage(messages.placeholder)} />
</div> </div>
</div> </div>
); );

View File

@ -70,7 +70,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<IconButton <IconButton
className='datepicker__button' className='datepicker__button'
src={require('@tabler/icons/icons/chevron-left.svg')} src={require('@tabler/icons/chevron-left.svg')}
onClick={decreaseMonth} onClick={decreaseMonth}
disabled={prevMonthButtonDisabled} disabled={prevMonthButtonDisabled}
aria-label={intl.formatMessage(messages.previousMonth)} aria-label={intl.formatMessage(messages.previousMonth)}
@ -79,7 +79,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
{intl.formatDate(date, { month: 'long' })} {intl.formatDate(date, { month: 'long' })}
<IconButton <IconButton
className='datepicker__button' className='datepicker__button'
src={require('@tabler/icons/icons/chevron-right.svg')} src={require('@tabler/icons/chevron-right.svg')}
onClick={increaseMonth} onClick={increaseMonth}
disabled={nextMonthButtonDisabled} disabled={nextMonthButtonDisabled}
aria-label={intl.formatMessage(messages.nextMonth)} aria-label={intl.formatMessage(messages.nextMonth)}
@ -89,7 +89,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<IconButton <IconButton
className='datepicker__button' className='datepicker__button'
src={require('@tabler/icons/icons/chevron-left.svg')} src={require('@tabler/icons/chevron-left.svg')}
onClick={decreaseYear} onClick={decreaseYear}
disabled={prevYearButtonDisabled} disabled={prevYearButtonDisabled}
aria-label={intl.formatMessage(messages.previousYear)} aria-label={intl.formatMessage(messages.previousYear)}
@ -98,7 +98,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
{intl.formatDate(date, { year: 'numeric' })} {intl.formatDate(date, { year: 'numeric' })}
<IconButton <IconButton
className='datepicker__button' className='datepicker__button'
src={require('@tabler/icons/icons/chevron-right.svg')} src={require('@tabler/icons/chevron-right.svg')}
onClick={increaseYear} onClick={increaseYear}
disabled={nextYearButtonDisabled} disabled={nextYearButtonDisabled}
aria-label={intl.formatMessage(messages.nextYear)} aria-label={intl.formatMessage(messages.nextYear)}

View File

@ -22,7 +22,7 @@ const DisplayName: React.FC<IDisplayName> = ({ account, children, withDate = fal
const joinedAt = createdAt ? ( const joinedAt = createdAt ? (
<div className='account__joined-at'> <div className='account__joined-at'>
<Icon src={require('@tabler/icons/icons/clock.svg')} /> <Icon src={require('@tabler/icons/clock.svg')} />
<RelativeTimestamp timestamp={createdAt} /> <RelativeTimestamp timestamp={createdAt} />
</div> </div>
) : null; ) : null;

View File

@ -21,7 +21,7 @@ const Domain: React.FC<IDomain> = ({ domain }) => {
// const onBlockDomain = () => { // const onBlockDomain = () => {
// dispatch(openModal('CONFIRM', { // dispatch(openModal('CONFIRM', {
// icon: require('@tabler/icons/icons/ban.svg'), // icon: require('@tabler/icons/ban.svg'),
// heading: <FormattedMessage id='confirmations.domain_block.heading' defaultMessage='Block {domain}' values={{ domain }} />, // heading: <FormattedMessage id='confirmations.domain_block.heading' defaultMessage='Block {domain}' values={{ domain }} />,
// message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />, // message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />,
// confirm: intl.formatMessage(messages.blockDomainConfirm), // confirm: intl.formatMessage(messages.blockDomainConfirm),
@ -41,7 +41,7 @@ const Domain: React.FC<IDomain> = ({ domain }) => {
</span> </span>
<div className='domain__buttons'> <div className='domain__buttons'>
<IconButton active src={require('@tabler/icons/icons/lock-open.svg')} title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={handleDomainUnblock} /> <IconButton active src={require('@tabler/icons/lock-open.svg')} title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={handleDomainUnblock} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -366,7 +366,7 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
} }
render() { render() {
const { src = require('@tabler/icons/icons/dots.svg'), items, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard = false, pressed, text, children } = this.props; const { src = require('@tabler/icons/dots.svg'), items, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard = false, pressed, text, children } = this.props;
const open = this.state.id === openDropdownId; const open = this.state.id === openDropdownId;
return ( return (

View File

@ -120,7 +120,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
{logo ? ( {logo ? (
<img className='h-12' src={logo} alt={siteTitle} /> <img className='h-12' src={logo} alt={siteTitle} />
) : ( ) : (
<SvgIcon className='h-12 w-12' src={require('@tabler/icons/icons/home.svg')} alt={siteTitle} /> <SvgIcon className='h-12 w-12' src={require('@tabler/icons/home.svg')} alt={siteTitle} />
)} )}
</a> </a>
</div> </div>

View File

@ -54,7 +54,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick }) => {
<div className='flex flex-row items-center text-gray-500 dark:text-gray-400'> <div className='flex flex-row items-center text-gray-500 dark:text-gray-400'>
{children} {children}
<Icon src={require('@tabler/icons/icons/chevron-right.svg')} className='ml-1' /> <Icon src={require('@tabler/icons/chevron-right.svg')} className='ml-1' />
</div> </div>
) : renderChildren()} ) : renderChildren()}
</Comp> </Comp>

View File

@ -149,7 +149,7 @@ class Item extends React.PureComponent {
const attachmentIcon = ( const attachmentIcon = (
<Icon <Icon
className='h-16 w-16 text-gray-800 dark:text-gray-200' className='h-16 w-16 text-gray-800 dark:text-gray-200'
src={MIMETYPE_ICONS[attachment.getIn(['pleroma', 'mime_type'])] || require('@tabler/icons/icons/paperclip.svg')} src={MIMETYPE_ICONS[attachment.getIn(['pleroma', 'mime_type'])] || require('@tabler/icons/paperclip.svg')}
/> />
); );
@ -602,7 +602,7 @@ class MediaGallery extends React.PureComponent {
(visible || compact) ? ( (visible || compact) ? (
<Button <Button
text={intl.formatMessage(messages.toggle_visible)} text={intl.formatMessage(messages.toggle_visible)}
icon={visible ? require('@tabler/icons/icons/eye-off.svg') : require('@tabler/icons/icons/eye.svg')} icon={visible ? require('@tabler/icons/eye-off.svg') : require('@tabler/icons/eye.svg')}
onClick={this.handleOpen} onClick={this.handleOpen}
theme='transparent' theme='transparent'
size='sm' size='sm'
@ -617,7 +617,7 @@ class MediaGallery extends React.PureComponent {
</Text> </Text>
</div> </div>
<Button type='button' theme='primary' size='sm' icon={require('@tabler/icons/icons/eye.svg')}> <Button type='button' theme='primary' size='sm' icon={require('@tabler/icons/eye.svg')}>
<FormattedMessage id='status.sensitive_warning.action' defaultMessage='Show content' /> <FormattedMessage id='status.sensitive_warning.action' defaultMessage='Show content' />
</Button> </Button>
</div> </div>

View File

@ -78,7 +78,7 @@ class ModalRoot extends React.PureComponent {
if (hasComposeContent && type === 'COMPOSE') { if (hasComposeContent && type === 'COMPOSE') {
onOpenModal('CONFIRM', { onOpenModal('CONFIRM', {
icon: require('@tabler/icons/icons/trash.svg'), icon: require('@tabler/icons/trash.svg'),
heading: isEditing ? <FormattedMessage id='confirmations.cancel_editing.heading' defaultMessage='Cancel post editing' /> : <FormattedMessage id='confirmations.delete.heading' defaultMessage='Delete post' />, heading: isEditing ? <FormattedMessage id='confirmations.cancel_editing.heading' defaultMessage='Cancel post editing' /> : <FormattedMessage id='confirmations.delete.heading' defaultMessage='Delete post' />,
message: isEditing ? <FormattedMessage id='confirmations.cancel_editing.message' defaultMessage='Are you sure you want to cancel editing this post? All changes will be lost.' /> : <FormattedMessage id='confirmations.delete.message' defaultMessage='Are you sure you want to delete this post?' />, message: isEditing ? <FormattedMessage id='confirmations.cancel_editing.message' defaultMessage='Are you sure you want to cancel editing this post? All changes will be lost.' /> : <FormattedMessage id='confirmations.delete.message' defaultMessage='Are you sure you want to delete this post?' />,
confirm: intl.formatMessage(messages.confirm), confirm: intl.formatMessage(messages.confirm),

View File

@ -6,7 +6,7 @@ import { Provider } from 'react-redux';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { normalizePoll } from 'soapbox/normalizers/poll'; import { normalizePoll } from 'soapbox/normalizers/poll';
import { mockStore, render, rootReducer, screen } from '../../../jest/test-helpers'; import { mockStore, render, screen, rootState } from '../../../jest/test-helpers';
import PollFooter from '../poll-footer'; import PollFooter from '../poll-footer';
let poll = normalizePoll({ let poll = normalizePoll({
@ -36,7 +36,7 @@ describe('<PollFooter />', () => {
}); });
const user = userEvent.setup(); const user = userEvent.setup();
const store = mockStore(rootReducer(undefined, {})); const store = mockStore(rootState);
render( render(
<Provider store={store}> <Provider store={store}>
<IntlProvider locale='en'> <IntlProvider locale='en'>

View File

@ -85,7 +85,7 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
aria-label={option.title} aria-label={option.title}
> >
{active && ( {active && (
<Icon src={require('@tabler/icons/icons/check.svg')} className='text-white dark:text-primary-900 w-4 h-4' /> <Icon src={require('@tabler/icons/check.svg')} className='text-white dark:text-primary-900 w-4 h-4' />
)} )}
</span> </span>
</div> </div>
@ -138,7 +138,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
<HStack space={2} alignItems='center' className='relative'> <HStack space={2} alignItems='center' className='relative'>
{voted ? ( {voted ? (
<Icon <Icon
src={require('@tabler/icons/icons/circle-check.svg')} src={require('@tabler/icons/circle-check.svg')}
alt={intl.formatMessage(messages.voted)} alt={intl.formatMessage(messages.voted)}
className='text-primary-600 dark:text-primary-800 dark:fill-white w-4 h-4' className='text-primary-600 dark:text-primary-800 dark:fill-white w-4 h-4'
/> />

View File

@ -116,7 +116,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
if (onCancel) { if (onCancel) {
actions = { actions = {
onActionClick: handleClose, onActionClick: handleClose,
actionIcon: require('@tabler/icons/icons/x.svg'), actionIcon: require('@tabler/icons/x.svg'),
actionAlignment: 'top', actionAlignment: 'top',
actionTitle: intl.formatMessage(messages.cancel), actionTitle: intl.formatMessage(messages.cancel),
}; };
@ -137,6 +137,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
timestamp={status.created_at} timestamp={status.created_at}
withRelationship={false} withRelationship={false}
showProfileHoverCard={!compose} showProfileHoverCard={!compose}
withLinkToProfile={!compose}
/> />
{renderReplyMentions()} {renderReplyMentions()}

View File

@ -34,6 +34,12 @@ const ScrollTopButton: React.FC<IScrollTopButton> = ({
const [scrolled, setScrolled] = useState<boolean>(false); const [scrolled, setScrolled] = useState<boolean>(false);
const autoload = settings.get('autoloadTimelines') === true; const autoload = settings.get('autoloadTimelines') === true;
const visible = count > 0 && scrolled;
const classes = classNames('left-1/2 -translate-x-1/2 fixed top-20 z-50', {
'hidden': !visible,
});
const getScrollTop = (): number => { const getScrollTop = (): number => {
return (document.scrollingElement || document.documentElement).scrollTop; return (document.scrollingElement || document.documentElement).scrollTop;
}; };
@ -75,16 +81,10 @@ const ScrollTopButton: React.FC<IScrollTopButton> = ({
maybeUnload(); maybeUnload();
}, [count]); }, [count]);
const visible = count > 0 && scrolled;
const classes = classNames('left-1/2 -translate-x-1/2 fixed top-20 z-50', {
'hidden': !visible,
});
return ( return (
<div className={classes}> <div className={classes}>
<a className='flex items-center bg-primary-600 hover:bg-primary-700 hover:scale-105 active:scale-100 transition-transform text-white rounded-full px-4 py-2 space-x-1.5 cursor-pointer whitespace-nowrap' onClick={handleClick}> <a className='flex items-center bg-primary-600 hover:bg-primary-700 hover:scale-105 active:scale-100 transition-transform text-white rounded-full px-4 py-2 space-x-1.5 cursor-pointer whitespace-nowrap' onClick={handleClick}>
<Icon src={require('@tabler/icons/icons/arrow-bar-to-up.svg')} /> <Icon src={require('@tabler/icons/arrow-bar-to-up.svg')} />
{(count > 0) && ( {(count > 0) && (
<Text theme='inherit' size='sm'> <Text theme='inherit' size='sm'>

View File

@ -43,7 +43,7 @@ const SidebarNavigation = () => {
menu.push({ menu.push({
to: '/follow_requests', to: '/follow_requests',
text: intl.formatMessage(messages.follow_requests), text: intl.formatMessage(messages.follow_requests),
icon: require('@tabler/icons/icons/user-plus.svg'), icon: require('@tabler/icons/user-plus.svg'),
count: followRequestsCount, count: followRequestsCount,
}); });
} }
@ -52,7 +52,7 @@ const SidebarNavigation = () => {
menu.push({ menu.push({
to: '/bookmarks', to: '/bookmarks',
text: intl.formatMessage(messages.bookmarks), text: intl.formatMessage(messages.bookmarks),
icon: require('@tabler/icons/icons/bookmark.svg'), icon: require('@tabler/icons/bookmark.svg'),
}); });
} }
@ -60,14 +60,14 @@ const SidebarNavigation = () => {
menu.push({ menu.push({
to: '/lists', to: '/lists',
text: intl.formatMessage(messages.lists), text: intl.formatMessage(messages.lists),
icon: require('@tabler/icons/icons/list.svg'), icon: require('@tabler/icons/list.svg'),
}); });
} }
if (settings.get('isDeveloper')) { if (settings.get('isDeveloper')) {
menu.push({ menu.push({
to: '/developers', to: '/developers',
icon: require('@tabler/icons/icons/code.svg'), icon: require('@tabler/icons/code.svg'),
text: intl.formatMessage(messages.developers), text: intl.formatMessage(messages.developers),
}); });
} }
@ -75,7 +75,7 @@ const SidebarNavigation = () => {
if (account.staff) { if (account.staff) {
menu.push({ menu.push({
to: '/soapbox/admin', to: '/soapbox/admin',
icon: require('@tabler/icons/icons/dashboard.svg'), icon: require('@tabler/icons/dashboard.svg'),
text: intl.formatMessage(messages.dashboard), text: intl.formatMessage(messages.dashboard),
count: dashboardCount, count: dashboardCount,
}); });
@ -89,7 +89,7 @@ const SidebarNavigation = () => {
if (features.publicTimeline) { if (features.publicTimeline) {
menu.push({ menu.push({
to: '/timeline/local', to: '/timeline/local',
icon: features.federating ? require('@tabler/icons/icons/users.svg') : require('@tabler/icons/icons/world.svg'), icon: features.federating ? require('@tabler/icons/users.svg') : require('@tabler/icons/world.svg'),
text: features.federating ? instance.title : intl.formatMessage(messages.all), text: features.federating ? instance.title : intl.formatMessage(messages.all),
}); });
} }
@ -113,7 +113,7 @@ const SidebarNavigation = () => {
return ( return (
<SidebarNavigationLink <SidebarNavigationLink
to='/chats' to='/chats'
icon={require('@tabler/icons/icons/messages.svg')} icon={require('@tabler/icons/messages.svg')}
count={chatsCount} count={chatsCount}
text={<FormattedMessage id='tabs_bar.chats' defaultMessage='Chats' />} text={<FormattedMessage id='tabs_bar.chats' defaultMessage='Chats' />}
/> />
@ -124,7 +124,7 @@ const SidebarNavigation = () => {
return ( return (
<SidebarNavigationLink <SidebarNavigationLink
to='/messages' to='/messages'
icon={require('@tabler/icons/icons/mail.svg')} icon={require('@tabler/icons/mail.svg')}
text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />} text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
/> />
); );
@ -138,13 +138,13 @@ const SidebarNavigation = () => {
<div className='flex flex-col space-y-2'> <div className='flex flex-col space-y-2'>
<SidebarNavigationLink <SidebarNavigationLink
to='/' to='/'
icon={require('@tabler/icons/icons/home.svg')} icon={require('@tabler/icons/home.svg')}
text={<FormattedMessage id='tabs_bar.home' defaultMessage='Home' />} text={<FormattedMessage id='tabs_bar.home' defaultMessage='Home' />}
/> />
<SidebarNavigationLink <SidebarNavigationLink
to='/search' to='/search'
icon={require('@tabler/icons/icons/search.svg')} icon={require('@tabler/icons/search.svg')}
text={<FormattedMessage id='tabs_bar.search' defaultMessage='Search' />} text={<FormattedMessage id='tabs_bar.search' defaultMessage='Search' />}
/> />
@ -152,7 +152,7 @@ const SidebarNavigation = () => {
<> <>
<SidebarNavigationLink <SidebarNavigationLink
to='/notifications' to='/notifications'
icon={require('@tabler/icons/icons/bell.svg')} icon={require('@tabler/icons/bell.svg')}
count={notificationCount} count={notificationCount}
text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />} text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />}
/> />
@ -161,13 +161,13 @@ const SidebarNavigation = () => {
<SidebarNavigationLink <SidebarNavigationLink
to={`/@${account.acct}`} to={`/@${account.acct}`}
icon={require('@tabler/icons/icons/user.svg')} icon={require('@tabler/icons/user.svg')}
text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />} text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />}
/> />
<SidebarNavigationLink <SidebarNavigationLink
to='/settings' to='/settings'
icon={require('@tabler/icons/icons/settings.svg')} icon={require('@tabler/icons/settings.svg')}
text={<FormattedMessage id='tabs_bar.settings' defaultMessage='Settings' />} text={<FormattedMessage id='tabs_bar.settings' defaultMessage='Settings' />}
/> />
</> </>
@ -176,7 +176,7 @@ const SidebarNavigation = () => {
{menu.length > 0 && ( {menu.length > 0 && (
<DropdownMenu items={menu}> <DropdownMenu items={menu}>
<SidebarNavigationLink <SidebarNavigationLink
icon={require('@tabler/icons/icons/dots-circle-horizontal.svg')} icon={require('@tabler/icons/dots-circle-horizontal.svg')}
count={dashboardCount} count={dashboardCount}
text={<FormattedMessage id='tabs_bar.more' defaultMessage='More' />} text={<FormattedMessage id='tabs_bar.more' defaultMessage='More' />}
/> />

View File

@ -121,7 +121,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
const renderAccount = (account: AccountEntity) => ( const renderAccount = (account: AccountEntity) => (
<a href='#' className='block py-2' onClick={handleSwitchAccount(account)} key={account.id}> <a href='#' className='block py-2' onClick={handleSwitchAccount(account)} key={account.id}>
<div className='pointer-events-none'> <div className='pointer-events-none'>
<Account account={account} showProfileHoverCard={false} withRelationship={false} /> <Account account={account} showProfileHoverCard={false} withRelationship={false} withLinkToProfile={false} />
</div> </div>
</a> </a>
); );
@ -158,7 +158,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<IconButton <IconButton
title='close' title='close'
onClick={handleClose} onClick={handleClose}
src={require('@tabler/icons/icons/x.svg')} src={require('@tabler/icons/x.svg')}
ref={closeButtonRef} ref={closeButtonRef}
className='text-gray-400 hover:text-gray-600' className='text-gray-400 hover:text-gray-600'
/> />
@ -166,7 +166,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<Stack space={1}> <Stack space={1}>
<Link to={`/@${account.acct}`} onClick={onClose}> <Link to={`/@${account.acct}`} onClick={onClose}>
<Account account={account} showProfileHoverCard={false} /> <Account account={account} showProfileHoverCard={false} withLinkToProfile={false} />
</Link> </Link>
<Stack> <Stack>
@ -177,7 +177,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
</Text> </Text>
<Icon <Icon
src={require('@tabler/icons/icons/chevron-down.svg')} src={require('@tabler/icons/chevron-down.svg')}
className={classNames('text-black dark:text-white transition-transform', { className={classNames('text-black dark:text-white transition-transform', {
'rotate-180': switcher, 'rotate-180': switcher,
})} })}
@ -190,7 +190,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
{otherAccounts.map(account => renderAccount(account))} {otherAccounts.map(account => renderAccount(account))}
<NavLink className='flex py-2 space-x-1' to='/login/add' onClick={handleClose}> <NavLink className='flex py-2 space-x-1' to='/login/add' onClick={handleClose}>
<Icon className='dark:text-white' src={require('@tabler/icons/icons/plus.svg')} /> <Icon className='dark:text-white' src={require('@tabler/icons/plus.svg')} />
<Text>{intl.formatMessage(messages.addAccount)}</Text> <Text>{intl.formatMessage(messages.addAccount)}</Text>
</NavLink> </NavLink>
</div> </div>
@ -208,7 +208,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<SidebarLink <SidebarLink
to={`/@${account.acct}`} to={`/@${account.acct}`}
icon={require('@tabler/icons/icons/user.svg')} icon={require('@tabler/icons/user.svg')}
text={intl.formatMessage(messages.profile)} text={intl.formatMessage(messages.profile)}
onClick={onClose} onClick={onClose}
/> />
@ -216,7 +216,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
{features.bookmarks && ( {features.bookmarks && (
<SidebarLink <SidebarLink
to='/bookmarks' to='/bookmarks'
icon={require('@tabler/icons/icons/bookmark.svg')} icon={require('@tabler/icons/bookmark.svg')}
text={intl.formatMessage(messages.bookmarks)} text={intl.formatMessage(messages.bookmarks)}
onClick={onClose} onClick={onClose}
/> />
@ -225,7 +225,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
{features.lists && ( {features.lists && (
<SidebarLink <SidebarLink
to='/lists' to='/lists'
icon={require('@tabler/icons/icons/list.svg')} icon={require('@tabler/icons/list.svg')}
text={intl.formatMessage(messages.lists)} text={intl.formatMessage(messages.lists)}
onClick={onClose} onClick={onClose}
/> />
@ -234,7 +234,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
{settings.get('isDeveloper') && ( {settings.get('isDeveloper') && (
<SidebarLink <SidebarLink
to='/developers' to='/developers'
icon={require('@tabler/icons/icons/code.svg')} icon={require('@tabler/icons/code.svg')}
text={intl.formatMessage(messages.developers)} text={intl.formatMessage(messages.developers)}
onClick={onClose} onClick={onClose}
/> />
@ -245,7 +245,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<SidebarLink <SidebarLink
to='/timeline/local' to='/timeline/local'
icon={features.federating ? require('@tabler/icons/icons/users.svg') : require('@tabler/icons/icons/world.svg')} icon={features.federating ? require('@tabler/icons/users.svg') : require('@tabler/icons/world.svg')}
text={features.federating ? instance.title : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />} text={features.federating ? instance.title : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
onClick={onClose} onClick={onClose}
/> />
@ -264,21 +264,21 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<SidebarLink <SidebarLink
to='/blocks' to='/blocks'
icon={require('@tabler/icons/icons/ban.svg')} icon={require('@tabler/icons/ban.svg')}
text={intl.formatMessage(messages.blocks)} text={intl.formatMessage(messages.blocks)}
onClick={onClose} onClick={onClose}
/> />
<SidebarLink <SidebarLink
to='/mutes' to='/mutes'
icon={require('@tabler/icons/icons/circle-x.svg')} icon={require('@tabler/icons/circle-x.svg')}
text={intl.formatMessage(messages.mutes)} text={intl.formatMessage(messages.mutes)}
onClick={onClose} onClick={onClose}
/> />
<SidebarLink <SidebarLink
to='/settings/preferences' to='/settings/preferences'
icon={require('@tabler/icons/icons/settings.svg')} icon={require('@tabler/icons/settings.svg')}
text={intl.formatMessage(messages.preferences)} text={intl.formatMessage(messages.preferences)}
onClick={onClose} onClick={onClose}
/> />
@ -286,7 +286,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
{features.federating && ( {features.federating && (
<SidebarLink <SidebarLink
to='/domain_blocks' to='/domain_blocks'
icon={require('@tabler/icons/icons/ban.svg')} icon={require('@tabler/icons/ban.svg')}
text={intl.formatMessage(messages.domainBlocks)} text={intl.formatMessage(messages.domainBlocks)}
onClick={onClose} onClick={onClose}
/> />
@ -295,7 +295,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
{features.filters && ( {features.filters && (
<SidebarLink <SidebarLink
to='/filters' to='/filters'
icon={require('@tabler/icons/icons/filter.svg')} icon={require('@tabler/icons/filter.svg')}
text={intl.formatMessage(messages.filters)} text={intl.formatMessage(messages.filters)}
onClick={onClose} onClick={onClose}
/> />
@ -304,7 +304,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
{account.admin && ( {account.admin && (
<SidebarLink <SidebarLink
to='/soapbox/config' to='/soapbox/config'
icon={require('@tabler/icons/icons/settings.svg')} icon={require('@tabler/icons/settings.svg')}
text={intl.formatMessage(messages.soapboxConfig)} text={intl.formatMessage(messages.soapboxConfig)}
onClick={onClose} onClick={onClose}
/> />
@ -313,7 +313,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
{features.import && ( {features.import && (
<SidebarLink <SidebarLink
to='/settings/import' to='/settings/import'
icon={require('@tabler/icons/icons/cloud-upload.svg')} icon={require('@tabler/icons/cloud-upload.svg')}
text={intl.formatMessage(messages.importData)} text={intl.formatMessage(messages.importData)}
onClick={onClose} onClick={onClose}
/> />
@ -323,7 +323,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<SidebarLink <SidebarLink
to='/logout' to='/logout'
icon={require('@tabler/icons/icons/logout.svg')} icon={require('@tabler/icons/logout.svg')}
text={intl.formatMessage(messages.logout)} text={intl.formatMessage(messages.logout)}
onClick={onClickLogOut} onClick={onClickLogOut}
/> />

View File

@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import { Text, Icon } from 'soapbox/components/ui'; import { Text, Icon, Emoji } from 'soapbox/components/ui';
import { shortNumberFormat } from 'soapbox/utils/numbers'; import { shortNumberFormat } from 'soapbox/utils/numbers';
const COLORS = { const COLORS = {
@ -15,7 +15,7 @@ interface IStatusActionCounter {
count: number, count: number,
} }
/** Action button numerical counter, eg "5" likes */ /** Action button numerical counter, eg "5" likes. */
const StatusActionCounter: React.FC<IStatusActionCounter> = ({ count = 0 }): JSX.Element => { const StatusActionCounter: React.FC<IStatusActionCounter> = ({ count = 0 }): JSX.Element => {
return ( return (
<Text size='xs' weight='semibold' theme='inherit'> <Text size='xs' weight='semibold' theme='inherit'>
@ -31,10 +31,11 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes<HTMLButtonEleme
active?: boolean, active?: boolean,
color?: Color, color?: Color,
filled?: boolean, filled?: boolean,
emoji?: string,
} }
const StatusActionButton = React.forwardRef((props: IStatusActionButton, ref: React.ForwardedRef<HTMLButtonElement>): JSX.Element => { const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButton>((props, ref): JSX.Element => {
const { icon, className, iconClassName, active, color, filled = false, count = 0, ...filteredProps } = props; const { icon, className, iconClassName, active, color, filled = false, count = 0, emoji, ...filteredProps } = props;
return ( return (
<button <button
@ -46,13 +47,19 @@ const StatusActionButton = React.forwardRef((props: IStatusActionButton, ref: Re
'bg-white dark:bg-transparent', 'bg-white dark:bg-transparent',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:ring-offset-0', 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:ring-offset-0',
{ {
'text-accent-300 hover:text-accent-300 dark:hover:text-accent-300': active && color === COLORS.accent, 'text-black dark:text-white': active && emoji,
'text-success-600 hover:text-success-600 dark:hover:text-success-600': active && color === COLORS.success, 'text-accent-300 hover:text-accent-300 dark:hover:text-accent-300': active && !emoji && color === COLORS.accent,
'text-success-600 hover:text-success-600 dark:hover:text-success-600': active && !emoji && color === COLORS.success,
}, },
className, className,
)} )}
{...filteredProps} {...filteredProps}
> >
{emoji ? (
<span className='block w-6 h-6 flex items-center justify-center'>
<Emoji className='w-full h-full p-0.5' emoji={emoji} />
</span>
) : (
<Icon <Icon
src={icon} src={icon}
className={classNames( className={classNames(
@ -62,6 +69,7 @@ const StatusActionButton = React.forwardRef((props: IStatusActionButton, ref: Re
iconClassName, iconClassName,
)} )}
/> />
)}
{(count || null) && ( {(count || null) && (
<StatusActionCounter count={count} /> <StatusActionCounter count={count} />

View File

@ -84,8 +84,6 @@ interface IStatus extends RouteComponentProps {
onMoveDown: (statusId: string, featured?: boolean) => void, onMoveDown: (statusId: string, featured?: boolean) => void,
getScrollPosition?: () => ScrollPosition | undefined, getScrollPosition?: () => ScrollPosition | undefined,
updateScrollBottom?: (bottom: number) => void, updateScrollBottom?: (bottom: number) => void,
cacheMediaWidth: () => void,
cachedMediaWidth: number,
group: ImmutableMap<string, any>, group: ImmutableMap<string, any>,
displayMedia: string, displayMedia: string,
allowedEmoji: ImmutableList<string>, allowedEmoji: ImmutableList<string>,
@ -134,11 +132,11 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
this.didShowCard = Boolean(!this.props.muted && !this.props.hidden && this.props.status && this.props.status.card); this.didShowCard = Boolean(!this.props.muted && !this.props.hidden && this.props.status && this.props.status.card);
} }
getSnapshotBeforeUpdate(): ScrollPosition | undefined { getSnapshotBeforeUpdate(): ScrollPosition | null {
if (this.props.getScrollPosition) { if (this.props.getScrollPosition) {
return this.props.getScrollPosition(); return this.props.getScrollPosition() || null;
} else { } else {
return undefined; return null;
} }
} }
@ -346,7 +344,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
prepend = ( prepend = (
<div className='pt-4 px-4'> <div className='pt-4 px-4'>
<HStack alignItems='center' space={1}> <HStack alignItems='center' space={1}>
<Icon src={require('@tabler/icons/icons/pinned.svg')} className='text-gray-600 dark:text-gray-400' /> <Icon src={require('@tabler/icons/pinned.svg')} className='text-gray-600 dark:text-gray-400' />
<Text size='sm' theme='muted' weight='medium'> <Text size='sm' theme='muted' weight='medium'>
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' /> <FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
@ -365,7 +363,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
className='hidden sm:flex items-center text-gray-500 text-xs font-medium space-x-1 hover:underline' className='hidden sm:flex items-center text-gray-500 text-xs font-medium space-x-1 hover:underline'
> >
<Icon src={require('@tabler/icons/icons/repeat.svg')} className='text-green-600' /> <Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />
<HStack alignItems='center'> <HStack alignItems='center'>
<FormattedMessage <FormattedMessage
@ -388,7 +386,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
className='flex items-center text-gray-500 text-xs font-medium space-x-1 hover:underline' className='flex items-center text-gray-500 text-xs font-medium space-x-1 hover:underline'
> >
<Icon src={require('@tabler/icons/icons/repeat.svg')} className='text-green-600' /> <Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />
<span> <span>
<FormattedMessage <FormattedMessage
@ -474,7 +472,6 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
{reblogElementMobile} {reblogElementMobile}
<div className='mb-4'> <div className='mb-4'>
<HStack justifyContent='between' alignItems='start'>
<AccountContainer <AccountContainer
key={String(status.getIn(['account', 'id']))} key={String(status.getIn(['account', 'id']))}
id={String(status.getIn(['account', 'id']))} id={String(status.getIn(['account', 'id']))}
@ -484,8 +481,8 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
hideActions={!reblogElement} hideActions={!reblogElement}
showEdit={!!status.edited_at} showEdit={!!status.edited_at}
showProfileHoverCard={this.props.hoverable} showProfileHoverCard={this.props.hoverable}
withLinkToProfile={this.props.hoverable}
/> />
</HStack>
</div> </div>
<div className='status__content-wrapper'> <div className='status__content-wrapper'>

View File

@ -377,21 +377,21 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
menu.push({ menu.push({
text: intl.formatMessage(messages.open), text: intl.formatMessage(messages.open),
action: this.handleOpen, action: this.handleOpen,
icon: require('@tabler/icons/icons/arrows-vertical.svg'), icon: require('@tabler/icons/arrows-vertical.svg'),
}); });
if (publicStatus) { if (publicStatus) {
menu.push({ menu.push({
text: intl.formatMessage(messages.copy), text: intl.formatMessage(messages.copy),
action: this.handleCopy, action: this.handleCopy,
icon: require('@tabler/icons/icons/link.svg'), icon: require('@tabler/icons/link.svg'),
}); });
if (features.embeds) { if (features.embeds) {
menu.push({ menu.push({
text: intl.formatMessage(messages.embed), text: intl.formatMessage(messages.embed),
action: this.handleEmbed, action: this.handleEmbed,
icon: require('@tabler/icons/icons/share.svg'), icon: require('@tabler/icons/share.svg'),
}); });
} }
} }
@ -404,7 +404,7 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
menu.push({ menu.push({
text: intl.formatMessage(status.bookmarked ? messages.unbookmark : messages.bookmark), text: intl.formatMessage(status.bookmarked ? messages.unbookmark : messages.bookmark),
action: this.handleBookmarkClick, action: this.handleBookmarkClick,
icon: require(status.bookmarked ? '@tabler/icons/icons/bookmark-off.svg' : '@tabler/icons/icons/bookmark.svg'), icon: status.bookmarked ? require('@tabler/icons/bookmark-off.svg') : require('@tabler/icons/bookmark.svg'),
}); });
} }
@ -414,7 +414,7 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
menu.push({ menu.push({
text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
action: this.handleConversationMuteClick, action: this.handleConversationMuteClick,
icon: require(mutingConversation ? '@tabler/icons/icons/bell.svg' : '@tabler/icons/icons/bell-off.svg'), icon: mutingConversation ? require('@tabler/icons/bell.svg') : require('@tabler/icons/bell-off.svg'),
}); });
menu.push(null); menu.push(null);
} }
@ -424,14 +424,14 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
menu.push({ menu.push({
text: intl.formatMessage(status.pinned ? messages.unpin : messages.pin), text: intl.formatMessage(status.pinned ? messages.unpin : messages.pin),
action: this.handlePinClick, action: this.handlePinClick,
icon: require(mutingConversation ? '@tabler/icons/icons/pinned-off.svg' : '@tabler/icons/icons/pin.svg'), icon: mutingConversation ? require('@tabler/icons/pinned-off.svg') : require('@tabler/icons/pin.svg'),
}); });
} else { } else {
if (status.visibility === 'private') { if (status.visibility === 'private') {
menu.push({ menu.push({
text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog_private), text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog_private),
action: this.handleReblogClick, action: this.handleReblogClick,
icon: require('@tabler/icons/icons/repeat.svg'), icon: require('@tabler/icons/repeat.svg'),
}); });
} }
} }
@ -439,20 +439,20 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
menu.push({ menu.push({
text: intl.formatMessage(messages.delete), text: intl.formatMessage(messages.delete),
action: this.handleDeleteClick, action: this.handleDeleteClick,
icon: require('@tabler/icons/icons/trash.svg'), icon: require('@tabler/icons/trash.svg'),
destructive: true, destructive: true,
}); });
if (features.editStatuses) { if (features.editStatuses) {
menu.push({ menu.push({
text: intl.formatMessage(messages.edit), text: intl.formatMessage(messages.edit),
action: this.handleEditClick, action: this.handleEditClick,
icon: require('@tabler/icons/icons/edit.svg'), icon: require('@tabler/icons/edit.svg'),
}); });
} else { } else {
menu.push({ menu.push({
text: intl.formatMessage(messages.redraft), text: intl.formatMessage(messages.redraft),
action: this.handleRedraftClick, action: this.handleRedraftClick,
icon: require('@tabler/icons/icons/edit.svg'), icon: require('@tabler/icons/edit.svg'),
destructive: true, destructive: true,
}); });
} }
@ -460,20 +460,20 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
menu.push({ menu.push({
text: intl.formatMessage(messages.mention, { name: username }), text: intl.formatMessage(messages.mention, { name: username }),
action: this.handleMentionClick, action: this.handleMentionClick,
icon: require('@tabler/icons/icons/at.svg'), icon: require('@tabler/icons/at.svg'),
}); });
// if (status.getIn(['account', 'pleroma', 'accepts_chat_messages'], false) === true) { // if (status.getIn(['account', 'pleroma', 'accepts_chat_messages'], false) === true) {
// menu.push({ // menu.push({
// text: intl.formatMessage(messages.chat, { name: username }), // text: intl.formatMessage(messages.chat, { name: username }),
// action: this.handleChatClick, // action: this.handleChatClick,
// icon: require('@tabler/icons/icons/messages.svg'), // icon: require('@tabler/icons/messages.svg'),
// }); // });
// } else { // } else {
// menu.push({ // menu.push({
// text: intl.formatMessage(messages.direct, { name: username }), // text: intl.formatMessage(messages.direct, { name: username }),
// action: this.handleDirectClick, // action: this.handleDirectClick,
// icon: require('@tabler/icons/icons/mail.svg'), // icon: require('@tabler/icons/mail.svg'),
// }); // });
// } // }
@ -481,17 +481,17 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
menu.push({ menu.push({
text: intl.formatMessage(messages.mute, { name: username }), text: intl.formatMessage(messages.mute, { name: username }),
action: this.handleMuteClick, action: this.handleMuteClick,
icon: require('@tabler/icons/icons/circle-x.svg'), icon: require('@tabler/icons/circle-x.svg'),
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.block, { name: username }), text: intl.formatMessage(messages.block, { name: username }),
action: this.handleBlockClick, action: this.handleBlockClick,
icon: require('@tabler/icons/icons/ban.svg'), icon: require('@tabler/icons/ban.svg'),
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.report, { name: username }), text: intl.formatMessage(messages.report, { name: username }),
action: this.handleReport, action: this.handleReport,
icon: require('@tabler/icons/icons/flag.svg'), icon: require('@tabler/icons/flag.svg'),
}); });
} }
@ -502,13 +502,13 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
menu.push({ menu.push({
text: intl.formatMessage(messages.admin_account, { name: username }), text: intl.formatMessage(messages.admin_account, { name: username }),
href: `/pleroma/admin/#/users/${status.getIn(['account', 'id'])}/`, href: `/pleroma/admin/#/users/${status.getIn(['account', 'id'])}/`,
icon: require('@tabler/icons/icons/gavel.svg'), icon: require('@tabler/icons/gavel.svg'),
action: (event) => event.stopPropagation(), action: (event) => event.stopPropagation(),
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.admin_status), text: intl.formatMessage(messages.admin_status),
href: `/pleroma/admin/#/statuses/${status.id}/`, href: `/pleroma/admin/#/statuses/${status.id}/`,
icon: require('@tabler/icons/icons/pencil.svg'), icon: require('@tabler/icons/pencil.svg'),
action: (event) => event.stopPropagation(), action: (event) => event.stopPropagation(),
}); });
} }
@ -516,25 +516,25 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
menu.push({ menu.push({
text: intl.formatMessage(status.sensitive === false ? messages.markStatusSensitive : messages.markStatusNotSensitive), text: intl.formatMessage(status.sensitive === false ? messages.markStatusSensitive : messages.markStatusNotSensitive),
action: this.handleToggleStatusSensitivity, action: this.handleToggleStatusSensitivity,
icon: require('@tabler/icons/icons/alert-triangle.svg'), icon: require('@tabler/icons/alert-triangle.svg'),
}); });
if (!ownAccount) { if (!ownAccount) {
menu.push({ menu.push({
text: intl.formatMessage(messages.deactivateUser, { name: username }), text: intl.formatMessage(messages.deactivateUser, { name: username }),
action: this.handleDeactivateUser, action: this.handleDeactivateUser,
icon: require('@tabler/icons/icons/user-off.svg'), icon: require('@tabler/icons/user-off.svg'),
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.deleteUser, { name: username }), text: intl.formatMessage(messages.deleteUser, { name: username }),
action: this.handleDeleteUser, action: this.handleDeleteUser,
icon: require('@tabler/icons/icons/user-minus.svg'), icon: require('@tabler/icons/user-minus.svg'),
destructive: true, destructive: true,
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.deleteStatus), text: intl.formatMessage(messages.deleteStatus),
action: this.handleDeleteStatus, action: this.handleDeleteStatus,
icon: require('@tabler/icons/icons/trash.svg'), icon: require('@tabler/icons/trash.svg'),
destructive: true, destructive: true,
}); });
} }
@ -545,13 +545,13 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
// menu.push({ // menu.push({
// text: intl.formatMessage(messages.group_remove_account), // text: intl.formatMessage(messages.group_remove_account),
// action: this.handleGroupRemoveAccount, // action: this.handleGroupRemoveAccount,
// icon: require('@tabler/icons/icons/user-x.svg'), // icon: require('@tabler/icons/user-x.svg'),
// destructive: true, // destructive: true,
// }); // });
// menu.push({ // menu.push({
// text: intl.formatMessage(messages.group_remove_post), // text: intl.formatMessage(messages.group_remove_post),
// action: this.handleGroupRemovePost, // action: this.handleGroupRemovePost,
// icon: require('@tabler/icons/icons/trash.svg'), // icon: require('@tabler/icons/trash.svg'),
// destructive: true, // destructive: true,
// }); // });
// } // }
@ -601,23 +601,23 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite); const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite);
const menu = this._makeMenu(publicStatus); const menu = this._makeMenu(publicStatus);
let reblogIcon = require('@tabler/icons/icons/repeat.svg'); let reblogIcon = require('@tabler/icons/repeat.svg');
let replyTitle; let replyTitle;
if (status.visibility === 'direct') { if (status.visibility === 'direct') {
reblogIcon = require('@tabler/icons/icons/mail.svg'); reblogIcon = require('@tabler/icons/mail.svg');
} else if (status.visibility === 'private') { } else if (status.visibility === 'private') {
reblogIcon = require('@tabler/icons/icons/lock.svg'); reblogIcon = require('@tabler/icons/lock.svg');
} }
const reblogMenu = [{ const reblogMenu = [{
text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog), text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog),
action: this.handleReblogClick, action: this.handleReblogClick,
icon: require('@tabler/icons/icons/repeat.svg'), icon: require('@tabler/icons/repeat.svg'),
}, { }, {
text: intl.formatMessage(messages.quotePost), text: intl.formatMessage(messages.quotePost),
action: this.handleQuoteClick, action: this.handleQuoteClick,
icon: require('@tabler/icons/icons/quote.svg'), icon: require('@tabler/icons/quote.svg'),
}]; }];
const reblogButton = ( const reblogButton = (
@ -644,7 +644,7 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
<div className='pt-4 flex flex-row space-x-2'> <div className='pt-4 flex flex-row space-x-2'>
<StatusActionButton <StatusActionButton
title={replyTitle} title={replyTitle}
icon={require('@tabler/icons/icons/message-circle-2.svg')} icon={require('@tabler/icons/message-circle-2.svg')}
onClick={this.handleReplyClick} onClick={this.handleReplyClick}
count={replyCount} count={replyCount}
/> />
@ -665,17 +665,18 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
<EmojiButtonWrapper statusId={status.id}> <EmojiButtonWrapper statusId={status.id}>
<StatusActionButton <StatusActionButton
title={meEmojiTitle} title={meEmojiTitle}
icon={require('@tabler/icons/icons/heart.svg')} icon={require('@tabler/icons/heart.svg')}
filled filled
color='accent' color='accent'
active={Boolean(meEmojiReact)} active={Boolean(meEmojiReact)}
count={emojiReactCount} count={emojiReactCount}
emoji={meEmojiReact}
/> />
</EmojiButtonWrapper> </EmojiButtonWrapper>
) : ( ) : (
<StatusActionButton <StatusActionButton
title={intl.formatMessage(messages.favourite)} title={intl.formatMessage(messages.favourite)}
icon={require('@tabler/icons/icons/heart.svg')} icon={require('@tabler/icons/heart.svg')}
color='accent' color='accent'
filled filled
onClick={this.handleFavouriteClick} onClick={this.handleFavouriteClick}
@ -687,7 +688,7 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
{canShare && ( {canShare && (
<StatusActionButton <StatusActionButton
title={intl.formatMessage(messages.share)} title={intl.formatMessage(messages.share)}
icon={require('@tabler/icons/icons/upload.svg')} icon={require('@tabler/icons/upload.svg')}
onClick={this.handleShareClick} onClick={this.handleShareClick}
/> />
)} )}
@ -695,7 +696,7 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
<DropdownMenuContainer items={menu} status={status}> <DropdownMenuContainer items={menu} status={status}>
<StatusActionButton <StatusActionButton
title={intl.formatMessage(messages.more)} title={intl.formatMessage(messages.more)}
icon={require('@tabler/icons/icons/dots.svg')} icon={require('@tabler/icons/dots.svg')}
/> />
</DropdownMenuContainer> </DropdownMenuContainer>
</div> </div>

View File

@ -6,6 +6,7 @@ import { FormattedMessage } from 'react-intl';
import LoadGap from 'soapbox/components/load_gap'; import LoadGap from 'soapbox/components/load_gap';
import ScrollableList from 'soapbox/components/scrollable_list'; import ScrollableList from 'soapbox/components/scrollable_list';
import StatusContainer from 'soapbox/containers/status_container'; import StatusContainer from 'soapbox/containers/status_container';
import FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions';
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
import PendingStatus from 'soapbox/features/ui/components/pending_status'; import PendingStatus from 'soapbox/features/ui/components/pending_status';
@ -77,7 +78,7 @@ const StatusList: React.FC<IStatusList> = ({
const handleLoadOlder = useCallback(debounce(() => { const handleLoadOlder = useCallback(debounce(() => {
const maxId = lastStatusId || statusIds.last(); const maxId = lastStatusId || statusIds.last();
if (onLoadMore && maxId) { if (onLoadMore && maxId) {
onLoadMore(maxId); onLoadMore(maxId.replace('末suggestions-', ''));
} }
}, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds.last()]); }, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds.last()]);
@ -149,11 +150,17 @@ const StatusList: React.FC<IStatusList> = ({
)); ));
}; };
const renderFeedSuggestions = (): React.ReactNode => {
return <FeedSuggestions key='suggestions' />;
};
const renderStatuses = (): React.ReactNode[] => { const renderStatuses = (): React.ReactNode[] => {
if (isLoading || statusIds.size > 0) { if (isLoading || statusIds.size > 0) {
return statusIds.toArray().map((statusId, index) => { return statusIds.toArray().map((statusId, index) => {
if (statusId === null) { if (statusId === null) {
return renderLoadGap(index); return renderLoadGap(index);
} else if (statusId.startsWith('末suggestions-')) {
return renderFeedSuggestions();
} else if (statusId.startsWith('末pending-')) { } else if (statusId.startsWith('末pending-')) {
return renderPendingStatus(statusId); return renderPendingStatus(statusId);
} else { } else {

View File

@ -17,7 +17,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
if (features.chats) { if (features.chats) {
return ( return (
<ThumbNavigationLink <ThumbNavigationLink
src={require('@tabler/icons/icons/messages.svg')} src={require('@tabler/icons/messages.svg')}
text={<FormattedMessage id='navigation.chats' defaultMessage='Chats' />} text={<FormattedMessage id='navigation.chats' defaultMessage='Chats' />}
to='/chats' to='/chats'
exact exact
@ -29,7 +29,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
if (features.directTimeline || features.conversations) { if (features.directTimeline || features.conversations) {
return ( return (
<ThumbNavigationLink <ThumbNavigationLink
src={require('@tabler/icons/icons/mail.svg')} src={require('@tabler/icons/mail.svg')}
text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />} text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
to='/messages' to='/messages'
paths={['/messages', '/conversations']} paths={['/messages', '/conversations']}
@ -43,14 +43,14 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
return ( return (
<div className='thumb-navigation'> <div className='thumb-navigation'>
<ThumbNavigationLink <ThumbNavigationLink
src={require('@tabler/icons/icons/home.svg')} src={require('@tabler/icons/home.svg')}
text={<FormattedMessage id='navigation.home' defaultMessage='Home' />} text={<FormattedMessage id='navigation.home' defaultMessage='Home' />}
to='/' to='/'
exact exact
/> />
<ThumbNavigationLink <ThumbNavigationLink
src={require('@tabler/icons/icons/search.svg')} src={require('@tabler/icons/search.svg')}
text={<FormattedMessage id='navigation.search' defaultMessage='Search' />} text={<FormattedMessage id='navigation.search' defaultMessage='Search' />}
to='/search' to='/search'
exact exact
@ -58,7 +58,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
{account && ( {account && (
<ThumbNavigationLink <ThumbNavigationLink
src={require('@tabler/icons/icons/bell.svg')} src={require('@tabler/icons/bell.svg')}
text={<FormattedMessage id='navigation.notifications' defaultMessage='Alerts' />} text={<FormattedMessage id='navigation.notifications' defaultMessage='Alerts' />}
to='/notifications' to='/notifications'
exact exact
@ -70,7 +70,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
{(account && account.staff) && ( {(account && account.staff) && (
<ThumbNavigationLink <ThumbNavigationLink
src={require('@tabler/icons/icons/dashboard.svg')} src={require('@tabler/icons/dashboard.svg')}
text={<FormattedMessage id='navigation.dashboard' defaultMessage='Dashboard' />} text={<FormattedMessage id='navigation.dashboard' defaultMessage='Dashboard' />}
to='/soapbox/admin' to='/soapbox/admin'
count={dashboardCount} count={dashboardCount}

View File

@ -64,7 +64,7 @@ const CardHeader: React.FC<ICardHeader> = ({ children, backHref, onBackClick }):
return ( return (
<Comp {...backAttributes} className='mr-2 text-gray-900 dark:text-gray-100 focus:ring-primary-500 focus:ring-2' aria-label={intl.formatMessage(messages.back)}> <Comp {...backAttributes} className='mr-2 text-gray-900 dark:text-gray-100 focus:ring-primary-500 focus:ring-2' aria-label={intl.formatMessage(messages.back)}>
<SvgIcon src={require('@tabler/icons/icons/arrow-left.svg')} className='h-6 w-6' /> <SvgIcon src={require('@tabler/icons/arrow-left.svg')} className='h-6 w-6' />
<span className='sr-only' data-testid='back-button'>{intl.formatMessage(messages.back)}</span> <span className='sr-only' data-testid='back-button'>{intl.formatMessage(messages.back)}</span>
</Comp> </Comp>
); );

View File

@ -30,7 +30,7 @@ describe('<Datepicker />', () => {
); );
let daySelect: HTMLElement; let daySelect: HTMLElement;
daySelect = document.querySelector('[data-testid="datepicker-day"]'); daySelect = document.querySelector('[data-testid="datepicker-day"]') as HTMLElement;
expect(queryAllByRole(daySelect, 'option')).toHaveLength(29); expect(queryAllByRole(daySelect, 'option')).toHaveLength(29);
await userEvent.selectOptions( await userEvent.selectOptions(
@ -56,26 +56,41 @@ describe('<Datepicker />', () => {
it('calls the onChange function when the inputs change', async() => { it('calls the onChange function when the inputs change', async() => {
const handler = jest.fn(); const handler = jest.fn();
render(<Datepicker onChange={handler} />); render(<Datepicker onChange={handler} />);
const today = new Date();
/**
* A date with a different day, month, and year than today
* so this test will always pass!
*/
const notToday = new Date(
today.getFullYear() - 1, // last year
(today.getMonth() + 2) % 11, // two months from now (mod 11 because it's 0-indexed)
(today.getDate() + 2) % 28, // 2 days from now (for timezone stuff)
);
const month = notToday.toLocaleString('en-us', { month: 'long' });
const year = String(notToday.getFullYear());
const day = String(notToday.getDate());
expect(handler.mock.calls.length).toEqual(1); expect(handler.mock.calls.length).toEqual(1);
await userEvent.selectOptions( await userEvent.selectOptions(
screen.getByTestId('datepicker-month'), screen.getByTestId('datepicker-month'),
screen.getByRole('option', { name: 'February' }), screen.getByRole('option', { name: month }),
); );
expect(handler.mock.calls.length).toEqual(2); expect(handler.mock.calls.length).toEqual(2);
await userEvent.selectOptions( await userEvent.selectOptions(
screen.getByTestId('datepicker-year'), screen.getByTestId('datepicker-year'),
screen.getByRole('option', { name: '2020' }), screen.getByRole('option', { name: year }),
); );
expect(handler.mock.calls.length).toEqual(3); expect(handler.mock.calls.length).toEqual(3);
await userEvent.selectOptions( await userEvent.selectOptions(
screen.getByTestId('datepicker-day'), screen.getByTestId('datepicker-day'),
screen.getByRole('option', { name: '5' }), screen.getByRole('option', { name: day }),
); );
expect(handler.mock.calls.length).toEqual(4); expect(handler.mock.calls.length).toEqual(4);

View File

@ -6,6 +6,7 @@ const justifyContentOptions = {
center: 'justify-center', center: 'justify-center',
start: 'justify-start', start: 'justify-start',
end: 'justify-end', end: 'justify-end',
around: 'justify-around',
}; };
const alignItemsOptions = { const alignItemsOptions = {
@ -32,7 +33,7 @@ interface IHStack {
/** Extra class names on the <div> element. */ /** Extra class names on the <div> element. */
className?: string, className?: string,
/** Horizontal alignment of children. */ /** Horizontal alignment of children. */
justifyContent?: 'between' | 'center' | 'start' | 'end', justifyContent?: 'between' | 'center' | 'start' | 'end' | 'around',
/** Size of the gap between elements. */ /** Size of the gap between elements. */
space?: 0.5 | 1 | 1.5 | 2 | 3 | 4 | 6 | 8, space?: 0.5 | 1 | 1.5 | 2 | 3 | 4 | 6 | 8,
/** Whether to let the flexbox grow. */ /** Whether to let the flexbox grow. */

View File

@ -25,6 +25,7 @@ const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef
type='button' type='button'
className={classNames('flex items-center space-x-2 p-1 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 dark:ring-offset-0 focus:ring-primary-500', { className={classNames('flex items-center space-x-2 p-1 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 dark:ring-offset-0 focus:ring-primary-500', {
'bg-white dark:bg-transparent': !transparent, 'bg-white dark:bg-transparent': !transparent,
'opacity-50': filteredProps.disabled,
}, className)} }, className)}
{...filteredProps} {...filteredProps}
data-testid='icon-button' data-testid='icon-button'

View File

@ -5,7 +5,7 @@ import SvgIcon from '../svg-icon';
describe('<SvgIcon />', () => { describe('<SvgIcon />', () => {
it('renders loading element with default size', () => { it('renders loading element with default size', () => {
render(<SvgIcon className='text-primary-500' src={require('@tabler/icons/icons/code.svg')} />); render(<SvgIcon className='text-primary-500' src={require('@tabler/icons/code.svg')} />);
const svg = screen.getByTestId('svg-icon-loader'); const svg = screen.getByTestId('svg-icon-loader');
expect(svg.getAttribute('width')).toBe('24'); expect(svg.getAttribute('width')).toBe('24');

View File

@ -88,7 +88,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
className='text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 h-full px-2 focus:ring-primary-500 focus:ring-2' className='text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 h-full px-2 focus:ring-primary-500 focus:ring-2'
> >
<SvgIcon <SvgIcon
src={revealed ? require('@tabler/icons/icons/eye-off.svg') : require('@tabler/icons/icons/eye.svg')} src={revealed ? require('@tabler/icons/eye-off.svg') : require('@tabler/icons/eye.svg')}
className='h-4 w-4' className='h-4 w-4'
/> />
</button> </button>

View File

@ -59,7 +59,7 @@ const Modal: React.FC<IModal> = ({
cancelAction, cancelAction,
cancelText, cancelText,
children, children,
closeIcon = require('@tabler/icons/icons/x.svg'), closeIcon = require('@tabler/icons/x.svg'),
closePosition = 'right', closePosition = 'right',
confirmationAction, confirmationAction,
confirmationDisabled, confirmationDisabled,
@ -83,7 +83,7 @@ const Modal: React.FC<IModal> = ({
}, [skipFocus, buttonRef]); }, [skipFocus, buttonRef]);
return ( return (
<div data-testid='modal' className={classNames('block w-full p-6 mx-auto overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-slate-800 text-black dark:text-white shadow-xl rounded-2xl pointer-events-auto', widths[width])}> <div data-testid='modal' className={classNames('block w-full p-6 mx-auto text-left align-middle transition-all transform bg-white dark:bg-slate-800 text-black dark:text-white shadow-xl rounded-2xl pointer-events-auto', widths[width])}>
<div className='sm:flex sm:items-start w-full justify-between'> <div className='sm:flex sm:items-start w-full justify-between'>
<div className='w-full'> <div className='w-full'>
{title && ( {title && (

View File

@ -1,9 +1,10 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
type SIZES = 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 10 type SIZES = 0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 10
const spaces = { const spaces = {
0: 'space-y-0',
'0.5': 'space-y-0.5', '0.5': 'space-y-0.5',
1: 'space-y-1', 1: 'space-y-1',
'1.5': 'space-y-1.5', '1.5': 'space-y-1.5',

View File

@ -74,7 +74,7 @@ const Streamfield: React.FC<IStreamfield> = ({
<IconButton <IconButton
iconClassName='w-4 h-4' iconClassName='w-4 h-4'
className='bg-transparent text-gray-400 hover:text-gray-600' className='bg-transparent text-gray-400 hover:text-gray-600'
src={require('@tabler/icons/icons/x.svg')} src={require('@tabler/icons/x.svg')}
onClick={() => onRemoveItem(i)} onClick={() => onRemoveItem(i)}
title={intl.formatMessage(messages.remove)} title={intl.formatMessage(messages.remove)}
/> />
@ -86,7 +86,7 @@ const Streamfield: React.FC<IStreamfield> = ({
{onAddItem && ( {onAddItem && (
<Button <Button
icon={require('@tabler/icons/icons/plus.svg')} icon={require('@tabler/icons/plus.svg')}
onClick={onAddItem} onClick={onAddItem}
disabled={values.length >= maxItems} disabled={values.length >= maxItems}
theme='ghost' theme='ghost'

View File

@ -6,3 +6,7 @@
@apply pointer-events-none absolute px-2.5 py-1.5 rounded shadow whitespace-nowrap text-xs font-medium bg-gray-800 text-white; @apply pointer-events-none absolute px-2.5 py-1.5 rounded shadow whitespace-nowrap text-xs font-medium bg-gray-800 text-white;
z-index: 100; z-index: 100;
} }
[data-reach-tooltip-arrow] {
@apply absolute z-50 w-0 h-0 border-l-8 border-solid border-l-transparent border-r-8 border-r-transparent border-b-8 border-b-gray-800;
}

View File

@ -1,4 +1,5 @@
import { default as ReachTooltip } from '@reach/tooltip'; import Portal from '@reach/portal';
import { TooltipPopup, useTooltip } from '@reach/tooltip';
import React from 'react'; import React from 'react';
import './tooltip.css'; import './tooltip.css';
@ -8,15 +9,55 @@ interface ITooltip {
text: string, text: string,
} }
const centered = (triggerRect: any, tooltipRect: any) => {
const triggerCenter = triggerRect.left + triggerRect.width / 2;
const left = triggerCenter - tooltipRect.width / 2;
const maxLeft = window.innerWidth - tooltipRect.width - 2;
return {
left: Math.min(Math.max(2, left), maxLeft) + window.scrollX,
top: triggerRect.bottom + 8 + window.scrollY,
};
};
/** Hoverable tooltip element. */ /** Hoverable tooltip element. */
const Tooltip: React.FC<ITooltip> = ({ const Tooltip: React.FC<ITooltip> = ({
children, children,
text, text,
}) => { }) => {
// get the props from useTooltip
const [trigger, tooltip] = useTooltip();
// destructure off what we need to position the triangle
const { isVisible, triggerRect } = tooltip;
return ( return (
<ReachTooltip label={text}> <React.Fragment>
{children} {React.cloneElement(children as any, trigger)}
</ReachTooltip>
{isVisible && (
// The Triangle. We position it relative to the trigger, not the popup
// so that collisions don't have a triangle pointing off to nowhere.
// Using a Portal may seem a little extreme, but we can keep the
// positioning logic simpler here instead of needing to consider
// the popup's position relative to the trigger and collisions
<Portal>
<div
data-reach-tooltip-arrow='true'
style={{
left:
triggerRect && triggerRect.left - 10 + triggerRect.width / 2 as any,
top: triggerRect && triggerRect.bottom + window.scrollY as any,
}}
/>
</Portal>
)}
<TooltipPopup
{...tooltip}
label={text}
aria-label={text}
position={centered}
/>
</React.Fragment>
); );
}; };

View File

@ -36,7 +36,7 @@ const Widget: React.FC<IWidget> = ({
title, title,
children, children,
onActionClick, onActionClick,
actionIcon = require('@tabler/icons/icons/arrow-right.svg'), actionIcon = require('@tabler/icons/arrow-right.svg'),
actionTitle, actionTitle,
action, action,
}): JSX.Element => { }): JSX.Element => {

View File

@ -15,7 +15,7 @@ const UploadProgress: React.FC<IUploadProgress> = ({ progress }) => {
return ( return (
<HStack alignItems='center' space={2}> <HStack alignItems='center' space={2}>
<Icon <Icon
src={require('@tabler/icons/icons/cloud-upload.svg')} src={require('@tabler/icons/cloud-upload.svg')}
className='w-7 h-7 text-gray-500' className='w-7 h-7 text-gray-500'
/> />

View File

@ -12,7 +12,7 @@ const ValidationCheckmark = ({ isValid, text }: IValidationCheckmark) => {
return ( return (
<HStack alignItems='center' space={2} data-testid='validation-checkmark'> <HStack alignItems='center' space={2} data-testid='validation-checkmark'>
<Icon <Icon
src={isValid ? require('@tabler/icons/icons/check.svg') : require('@tabler/icons/icons/point.svg')} src={isValid ? require('@tabler/icons/check.svg') : require('@tabler/icons/point.svg')}
className={classNames({ className={classNames({
'w-4 h-4': true, 'w-4 h-4': true,
'text-gray-400 fill-gray-400': !isValid, 'text-gray-400 fill-gray-400': !isValid,

View File

@ -38,7 +38,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (account.relationship?.following || account.relationship?.requested) { if (account.relationship?.following || account.relationship?.requested) {
if (unfollowModal) { if (unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/minus.svg'), icon: require('@tabler/icons/minus.svg'),
heading: <FormattedMessage id='confirmations.unfollow.heading' defaultMessage='Unfollow {name}' values={{ name: <strong>@{account.get('acct')}</strong> }} />, heading: <FormattedMessage id='confirmations.unfollow.heading' defaultMessage='Unfollow {name}' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm), confirm: intl.formatMessage(messages.unfollowConfirm),

View File

@ -5,6 +5,8 @@ import React, { useState, useEffect } from 'react';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { BrowserRouter, Switch, Redirect, Route } from 'react-router-dom'; import { BrowserRouter, Switch, Redirect, Route } from 'react-router-dom';
// @ts-ignore: it doesn't have types
import { ScrollContext } from 'react-router-scroll-4';
import { loadInstance } from 'soapbox/actions/instance'; import { loadInstance } from 'soapbox/actions/instance';
import { fetchMe } from 'soapbox/actions/me'; import { fetchMe } from 'soapbox/actions/me';
@ -115,6 +117,11 @@ const SoapboxMount = () => {
}); });
}, []); }, []);
// @ts-ignore: I don't actually know what these should be, lol
const shouldUpdateScroll = (prevRouterProps, { location }) => {
return !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey);
};
/** Whether to display a loading indicator. */ /** Whether to display a loading indicator. */
const showLoading = [ const showLoading = [
me === null, me === null,
@ -223,6 +230,7 @@ const SoapboxMount = () => {
{helmet} {helmet}
<ErrorBoundary> <ErrorBoundary>
<BrowserRouter basename={BuildConfig.FE_SUBDIRECTORY}> <BrowserRouter basename={BuildConfig.FE_SUBDIRECTORY}>
<ScrollContext shouldUpdateScroll={shouldUpdateScroll}>
<> <>
{renderBody()} {renderBody()}
@ -234,6 +242,7 @@ const SoapboxMount = () => {
{Component => <Component />} {Component => <Component />}
</BundleContainer> </BundleContainer>
</> </>
</ScrollContext>
</BrowserRouter> </BrowserRouter>
</ErrorBoundary> </ErrorBoundary>
</IntlProvider> </IntlProvider>

View File

@ -163,7 +163,7 @@ const mapDispatchToProps = (dispatch, { intl }) => {
dispatch(deleteStatus(status.get('id'), withRedraft)); dispatch(deleteStatus(status.get('id'), withRedraft));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: withRedraft ? require('@tabler/icons/icons/edit.svg') : require('@tabler/icons/icons/trash.svg'), icon: withRedraft ? require('@tabler/icons/edit.svg') : require('@tabler/icons/trash.svg'),
heading: intl.formatMessage(withRedraft ? messages.redraftHeading : messages.deleteHeading), heading: intl.formatMessage(withRedraft ? messages.redraftHeading : messages.deleteHeading),
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
@ -204,7 +204,7 @@ const mapDispatchToProps = (dispatch, { intl }) => {
onBlock(status) { onBlock(status) {
const account = status.get('account'); const account = status.get('account');
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/ban.svg'), icon: require('@tabler/icons/ban.svg'),
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.get('acct') }} />, heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.get('acct') }} />,
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm), confirm: intl.formatMessage(messages.blockConfirm),

View File

@ -175,7 +175,7 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.share, { name: account.get('username') }), text: intl.formatMessage(messages.share, { name: account.get('username') }),
action: this.handleShare, action: this.handleShare,
icon: require('@tabler/icons/icons/upload.svg'), icon: require('@tabler/icons/upload.svg'),
}); });
menu.push(null); menu.push(null);
} }
@ -184,53 +184,53 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.edit_profile), text: intl.formatMessage(messages.edit_profile),
to: '/settings/profile', to: '/settings/profile',
icon: require('@tabler/icons/icons/user.svg'), icon: require('@tabler/icons/user.svg'),
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.preferences), text: intl.formatMessage(messages.preferences),
to: '/settings', to: '/settings',
icon: require('@tabler/icons/icons/settings.svg'), icon: require('@tabler/icons/settings.svg'),
}); });
// menu.push(null); // menu.push(null);
// menu.push({ // menu.push({
// text: intl.formatMessage(messages.follow_requests), // text: intl.formatMessage(messages.follow_requests),
// to: '/follow_requests', // to: '/follow_requests',
// icon: require('@tabler/icons/icons/user-plus.svg'), // icon: require('@tabler/icons/user-plus.svg'),
// }); // });
menu.push(null); menu.push(null);
menu.push({ menu.push({
text: intl.formatMessage(messages.mutes), text: intl.formatMessage(messages.mutes),
to: '/mutes', to: '/mutes',
icon: require('@tabler/icons/icons/circle-x.svg'), icon: require('@tabler/icons/circle-x.svg'),
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.blocks), text: intl.formatMessage(messages.blocks),
to: '/blocks', to: '/blocks',
icon: require('@tabler/icons/icons/ban.svg'), icon: require('@tabler/icons/ban.svg'),
}); });
// menu.push({ // menu.push({
// text: intl.formatMessage(messages.domain_blocks), // text: intl.formatMessage(messages.domain_blocks),
// to: '/domain_blocks', // to: '/domain_blocks',
// icon: require('@tabler/icons/icons/ban.svg'), // icon: require('@tabler/icons/ban.svg'),
// }); // });
} else { } else {
menu.push({ menu.push({
text: intl.formatMessage(messages.mention, { name: account.get('username') }), text: intl.formatMessage(messages.mention, { name: account.get('username') }),
action: this.props.onMention, action: this.props.onMention,
icon: require('@tabler/icons/icons/at.svg'), icon: require('@tabler/icons/at.svg'),
}); });
// if (account.getIn(['pleroma', 'accepts_chat_messages'], false) === true) { // if (account.getIn(['pleroma', 'accepts_chat_messages'], false) === true) {
// menu.push({ // menu.push({
// text: intl.formatMessage(messages.chat, { name: account.get('username') }), // text: intl.formatMessage(messages.chat, { name: account.get('username') }),
// action: this.props.onChat, // action: this.props.onChat,
// icon: require('@tabler/icons/icons/messages.svg'), // icon: require('@tabler/icons/messages.svg'),
// }); // });
// } else { // } else {
// menu.push({ // menu.push({
// text: intl.formatMessage(messages.direct, { name: account.get('username') }), // text: intl.formatMessage(messages.direct, { name: account.get('username') }),
// action: this.props.onDirect, // action: this.props.onDirect,
// icon: require('@tabler/icons/icons/mail.svg'), // icon: require('@tabler/icons/mail.svg'),
// }); // });
// } // }
@ -239,13 +239,13 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }),
action: this.props.onReblogToggle, action: this.props.onReblogToggle,
icon: require('@tabler/icons/icons/repeat.svg'), icon: require('@tabler/icons/repeat.svg'),
}); });
} else { } else {
menu.push({ menu.push({
text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }),
action: this.props.onReblogToggle, action: this.props.onReblogToggle,
icon: require('@tabler/icons/icons/repeat.svg'), icon: require('@tabler/icons/repeat.svg'),
}); });
} }
@ -253,7 +253,7 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.add_or_remove_from_list), text: intl.formatMessage(messages.add_or_remove_from_list),
action: this.props.onAddToList, action: this.props.onAddToList,
icon: require('@tabler/icons/icons/list.svg'), icon: require('@tabler/icons/list.svg'),
}); });
} }
@ -263,7 +263,7 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.add_or_remove_from_list), text: intl.formatMessage(messages.add_or_remove_from_list),
action: this.props.onAddToList, action: this.props.onAddToList,
icon: require('@tabler/icons/icons/list.svg'), icon: require('@tabler/icons/list.svg'),
}); });
} }
@ -271,7 +271,7 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.removeFromFollowers), text: intl.formatMessage(messages.removeFromFollowers),
action: this.props.onRemoveFromFollowers, action: this.props.onRemoveFromFollowers,
icon: require('@tabler/icons/icons/user-x.svg'), icon: require('@tabler/icons/user-x.svg'),
}); });
} }
@ -279,13 +279,13 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.unmute, { name: account.get('username') }), text: intl.formatMessage(messages.unmute, { name: account.get('username') }),
action: this.props.onMute, action: this.props.onMute,
icon: require('@tabler/icons/icons/circle-x.svg'), icon: require('@tabler/icons/circle-x.svg'),
}); });
} else { } else {
menu.push({ menu.push({
text: intl.formatMessage(messages.mute, { name: account.get('username') }), text: intl.formatMessage(messages.mute, { name: account.get('username') }),
action: this.props.onMute, action: this.props.onMute,
icon: require('@tabler/icons/icons/circle-x.svg'), icon: require('@tabler/icons/circle-x.svg'),
}); });
} }
@ -293,20 +293,20 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.unblock, { name: account.get('username') }), text: intl.formatMessage(messages.unblock, { name: account.get('username') }),
action: this.props.onBlock, action: this.props.onBlock,
icon: require('@tabler/icons/icons/ban.svg'), icon: require('@tabler/icons/ban.svg'),
}); });
} else { } else {
menu.push({ menu.push({
text: intl.formatMessage(messages.block, { name: account.get('username') }), text: intl.formatMessage(messages.block, { name: account.get('username') }),
action: this.props.onBlock, action: this.props.onBlock,
icon: require('@tabler/icons/icons/ban.svg'), icon: require('@tabler/icons/ban.svg'),
}); });
} }
menu.push({ menu.push({
text: intl.formatMessage(messages.report, { name: account.get('username') }), text: intl.formatMessage(messages.report, { name: account.get('username') }),
action: this.props.onReport, action: this.props.onReport,
icon: require('@tabler/icons/icons/flag.svg'), icon: require('@tabler/icons/flag.svg'),
}); });
} }
@ -319,13 +319,13 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.unblockDomain, { domain }), text: intl.formatMessage(messages.unblockDomain, { domain }),
action: this.props.onUnblockDomain, action: this.props.onUnblockDomain,
icon: require('@tabler/icons/icons/ban.svg'), icon: require('@tabler/icons/ban.svg'),
}); });
} else { } else {
menu.push({ menu.push({
text: intl.formatMessage(messages.blockDomain, { domain }), text: intl.formatMessage(messages.blockDomain, { domain }),
action: this.props.onBlockDomain, action: this.props.onBlockDomain,
icon: require('@tabler/icons/icons/ban.svg'), icon: require('@tabler/icons/ban.svg'),
}); });
} }
} }
@ -338,7 +338,7 @@ class Header extends ImmutablePureComponent {
text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), text: intl.formatMessage(messages.admin_account, { name: account.get('username') }),
to: `/pleroma/admin/#/users/${account.id}/`, to: `/pleroma/admin/#/users/${account.id}/`,
newTab: true, newTab: true,
icon: require('@tabler/icons/icons/gavel.svg'), icon: require('@tabler/icons/gavel.svg'),
}); });
} }
@ -347,34 +347,34 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.demoteToModerator, { name: account.get('username') }), text: intl.formatMessage(messages.demoteToModerator, { name: account.get('username') }),
action: this.props.onPromoteToModerator, action: this.props.onPromoteToModerator,
icon: require('@tabler/icons/icons/arrow-up-circle.svg'), icon: require('@tabler/icons/arrow-up-circle.svg'),
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }), text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }),
action: this.props.onDemoteToUser, action: this.props.onDemoteToUser,
icon: require('@tabler/icons/icons/arrow-down-circle.svg'), icon: require('@tabler/icons/arrow-down-circle.svg'),
}); });
} else if (account.moderator) { } else if (account.moderator) {
menu.push({ menu.push({
text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }), text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }),
action: this.props.onPromoteToAdmin, action: this.props.onPromoteToAdmin,
icon: require('@tabler/icons/icons/arrow-up-circle.svg'), icon: require('@tabler/icons/arrow-up-circle.svg'),
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }), text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }),
action: this.props.onDemoteToUser, action: this.props.onDemoteToUser,
icon: require('@tabler/icons/icons/arrow-down-circle.svg'), icon: require('@tabler/icons/arrow-down-circle.svg'),
}); });
} else { } else {
menu.push({ menu.push({
text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }), text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }),
action: this.props.onPromoteToAdmin, action: this.props.onPromoteToAdmin,
icon: require('@tabler/icons/icons/arrow-up-circle.svg'), icon: require('@tabler/icons/arrow-up-circle.svg'),
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.promoteToModerator, { name: account.get('username') }), text: intl.formatMessage(messages.promoteToModerator, { name: account.get('username') }),
action: this.props.onPromoteToModerator, action: this.props.onPromoteToModerator,
icon: require('@tabler/icons/icons/arrow-up-circle.svg'), icon: require('@tabler/icons/arrow-up-circle.svg'),
}); });
} }
} }
@ -383,13 +383,13 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.unverifyUser, { name: account.username }), text: intl.formatMessage(messages.unverifyUser, { name: account.username }),
action: this.props.onUnverifyUser, action: this.props.onUnverifyUser,
icon: require('@tabler/icons/icons/check.svg'), icon: require('@tabler/icons/check.svg'),
}); });
} else { } else {
menu.push({ menu.push({
text: intl.formatMessage(messages.verifyUser, { name: account.username }), text: intl.formatMessage(messages.verifyUser, { name: account.username }),
action: this.props.onVerifyUser, action: this.props.onVerifyUser,
icon: require('@tabler/icons/icons/check.svg'), icon: require('@tabler/icons/check.svg'),
}); });
} }
@ -397,13 +397,13 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.removeDonor, { name: account.username }), text: intl.formatMessage(messages.removeDonor, { name: account.username }),
action: this.props.onRemoveDonor, action: this.props.onRemoveDonor,
icon: require('@tabler/icons/icons/coin.svg'), icon: require('@tabler/icons/coin.svg'),
}); });
} else { } else {
menu.push({ menu.push({
text: intl.formatMessage(messages.setDonor, { name: account.username }), text: intl.formatMessage(messages.setDonor, { name: account.username }),
action: this.props.onSetDonor, action: this.props.onSetDonor,
icon: require('@tabler/icons/icons/coin.svg'), icon: require('@tabler/icons/coin.svg'),
}); });
} }
@ -412,13 +412,13 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.unsuggestUser, { name: account.get('username') }), text: intl.formatMessage(messages.unsuggestUser, { name: account.get('username') }),
action: this.props.onUnsuggestUser, action: this.props.onUnsuggestUser,
icon: require('@tabler/icons/icons/user-x.svg'), icon: require('@tabler/icons/user-x.svg'),
}); });
} else { } else {
menu.push({ menu.push({
text: intl.formatMessage(messages.suggestUser, { name: account.get('username') }), text: intl.formatMessage(messages.suggestUser, { name: account.get('username') }),
action: this.props.onSuggestUser, action: this.props.onSuggestUser,
icon: require('@tabler/icons/icons/user-check.svg'), icon: require('@tabler/icons/user-check.svg'),
}); });
} }
} }
@ -427,11 +427,11 @@ class Header extends ImmutablePureComponent {
menu.push({ menu.push({
text: intl.formatMessage(messages.deactivateUser, { name: account.get('username') }), text: intl.formatMessage(messages.deactivateUser, { name: account.get('username') }),
action: this.props.onDeactivateUser, action: this.props.onDeactivateUser,
icon: require('@tabler/icons/icons/user-off.svg'), icon: require('@tabler/icons/user-off.svg'),
}); });
menu.push({ menu.push({
text: intl.formatMessage(messages.deleteUser, { name: account.get('username') }), text: intl.formatMessage(messages.deleteUser, { name: account.get('username') }),
icon: require('@tabler/icons/icons/user-minus.svg'), icon: require('@tabler/icons/user-minus.svg'),
}); });
} }
} }
@ -497,7 +497,7 @@ class Header extends ImmutablePureComponent {
if (canChat) { if (canChat) {
return ( return (
<IconButton <IconButton
src={require('@tabler/icons/icons/messages.svg')} src={require('@tabler/icons/messages.svg')}
onClick={this.props.onChat} onClick={this.props.onChat}
title={intl.formatMessage(messages.chat, { name: account.get('username') })} title={intl.formatMessage(messages.chat, { name: account.get('username') })}
/> />
@ -505,7 +505,7 @@ class Header extends ImmutablePureComponent {
} else { } else {
return ( return (
<IconButton <IconButton
src={require('@tabler/icons/icons/mail.svg')} src={require('@tabler/icons/mail.svg')}
onClick={this.props.onDirect} onClick={this.props.onDirect}
title={intl.formatMessage(messages.direct, { name: account.get('username') })} title={intl.formatMessage(messages.direct, { name: account.get('username') })}
className='text-primary-700 bg-primary-100 hover:bg-primary-200 p-2' className='text-primary-700 bg-primary-100 hover:bg-primary-200 p-2'
@ -525,7 +525,7 @@ class Header extends ImmutablePureComponent {
return ( return (
<IconButton <IconButton
src={require('@tabler/icons/icons/upload.svg')} src={require('@tabler/icons/upload.svg')}
onClick={this.handleShare} onClick={this.handleShare}
title={intl.formatMessage(messages.share, { name: account.get('username') })} title={intl.formatMessage(messages.share, { name: account.get('username') })}
className='text-primary-700 bg-primary-100 dark:!bg-slate-700 dark:!text-white hover:bg-primary-200 p-2' className='text-primary-700 bg-primary-100 dark:!bg-slate-700 dark:!text-white hover:bg-primary-200 p-2'
@ -602,7 +602,7 @@ class Header extends ImmutablePureComponent {
<Menu> <Menu>
<MenuButton <MenuButton
as={IconButton} as={IconButton}
src={require('@tabler/icons/icons/dots.svg')} src={require('@tabler/icons/dots.svg')}
className='text-primary-700 bg-primary-100 dark:!bg-slate-700 dark:!text-white hover:bg-primary-200 p-2' className='text-primary-700 bg-primary-100 dark:!bg-slate-700 dark:!text-white hover:bg-primary-200 p-2'
iconClassName='w-5 h-5' iconClassName='w-5 h-5'
/> />

View File

@ -31,7 +31,7 @@ class ColumnSettings extends React.PureComponent {
<FormattedMessage id='account.column_settings.title' defaultMessage='Account timeline settings' /> <FormattedMessage id='account.column_settings.title' defaultMessage='Account timeline settings' />
</h1> </h1>
<div className='column-settings__close'> <div className='column-settings__close'>
<IconButton title={intl.formatMessage(messages.close)} src={require('@tabler/icons/icons/x.svg')} onClick={onClose} /> <IconButton title={intl.formatMessage(messages.close)} src={require('@tabler/icons/x.svg')} onClick={onClose} />
</div> </div>
</div> </div>

View File

@ -96,7 +96,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(unblockAccount(account.get('id'))); dispatch(unblockAccount(account.get('id')));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/ban.svg'), icon: require('@tabler/icons/ban.svg'),
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.get('acct') }} />, heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.get('acct') }} />,
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm), confirm: intl.formatMessage(messages.blockConfirm),
@ -164,7 +164,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onBlockDomain(domain) { onBlockDomain(domain) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/ban.svg'), icon: require('@tabler/icons/ban.svg'),
heading: <FormattedMessage id='confirmations.domain_block.heading' defaultMessage='Block {domain}' values={{ domain }} />, heading: <FormattedMessage id='confirmations.domain_block.heading' defaultMessage='Block {domain}' values={{ domain }} />,
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.' values={{ domain: <strong>{domain}</strong> }} />, message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm), confirm: intl.formatMessage(messages.blockDomainConfirm),

View File

@ -40,11 +40,11 @@ const Report: React.FC<IReport> = ({ report }) => {
return [{ return [{
text: intl.formatMessage(messages.deactivateUser, { name: targetAccount.username as string }), text: intl.formatMessage(messages.deactivateUser, { name: targetAccount.username as string }),
action: handleDeactivateUser, action: handleDeactivateUser,
icon: require('@tabler/icons/icons/user-off.svg'), icon: require('@tabler/icons/user-off.svg'),
}, { }, {
text: intl.formatMessage(messages.deleteUser, { name: targetAccount.username as string }), text: intl.formatMessage(messages.deleteUser, { name: targetAccount.username as string }),
action: handleDeleteUser, action: handleDeleteUser,
icon: require('@tabler/icons/icons/user-minus.svg'), icon: require('@tabler/icons/user-minus.svg'),
}]; }];
}; };
@ -124,7 +124,7 @@ const Report: React.FC<IReport> = ({ report }) => {
<FormattedMessage id='admin.reports.actions.close' defaultMessage='Close' /> <FormattedMessage id='admin.reports.actions.close' defaultMessage='Close' />
</Button> </Button>
<DropdownMenu items={menu} src={require('@tabler/icons/icons/dots-vertical.svg')} /> <DropdownMenu items={menu} src={require('@tabler/icons/dots-vertical.svg')} />
</HStack> </HStack>
</div> </div>
); );

View File

@ -40,11 +40,11 @@ const ReportStatus: React.FC<IReportStatus> = ({ status }) => {
return [{ return [{
text: intl.formatMessage(messages.viewStatus, { acct: `@${acct}` }), text: intl.formatMessage(messages.viewStatus, { acct: `@${acct}` }),
to: `/@${acct}/posts/${status.id}`, to: `/@${acct}/posts/${status.id}`,
icon: require('@tabler/icons/icons/pencil.svg'), icon: require('@tabler/icons/pencil.svg'),
}, { }, {
text: intl.formatMessage(messages.deleteStatus, { acct: `@${acct}` }), text: intl.formatMessage(messages.deleteStatus, { acct: `@${acct}` }),
action: handleDeleteStatus, action: handleDeleteStatus,
icon: require('@tabler/icons/icons/trash.svg'), icon: require('@tabler/icons/trash.svg'),
destructive: true, destructive: true,
}]; }];
}; };
@ -123,7 +123,7 @@ const ReportStatus: React.FC<IReportStatus> = ({ status }) => {
<div className='admin-report__status-actions'> <div className='admin-report__status-actions'>
<DropdownMenu <DropdownMenu
items={menu} items={menu}
src={require('@tabler/icons/icons/dots-vertical.svg')} src={require('@tabler/icons/dots-vertical.svg')}
/> />
</div> </div>
</div> </div>

View File

@ -52,8 +52,8 @@ const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
<blockquote className='md'>{adminAccount?.invite_request || ''}</blockquote> <blockquote className='md'>{adminAccount?.invite_request || ''}</blockquote>
</div> </div>
<div className='unapproved-account__actions'> <div className='unapproved-account__actions'>
<IconButton src={require('@tabler/icons/icons/check.svg')} onClick={handleApprove} /> <IconButton src={require('@tabler/icons/check.svg')} onClick={handleApprove} />
<IconButton src={require('@tabler/icons/icons/x.svg')} onClick={handleReject} /> <IconButton src={require('@tabler/icons/x.svg')} onClick={handleReject} />
</div> </div>
</div> </div>
); );

View File

@ -48,7 +48,7 @@ const Account: React.FC<IAccount> = ({ accountId, aliases }) => {
if (!added && accountId !== me) { if (!added && accountId !== me) {
button = ( button = (
<div className='account__relationship'> <div className='account__relationship'>
<IconButton src={require('@tabler/icons/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={handleOnAdd} /> <IconButton src={require('@tabler/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={handleOnAdd} />
</div> </div>
); );
} }

View File

@ -54,7 +54,7 @@ const Search: React.FC = () => {
/> />
<div role='button' tabIndex={0} className='search__icon' onClick={handleClear}> <div role='button' tabIndex={0} className='search__icon' onClick={handleClear}>
<Icon src={require('@tabler/icons/icons/backspace.svg')} aria-label={intl.formatMessage(messages.search)} className={classNames('svg-icon--backspace', { active: hasValue })} /> <Icon src={require('@tabler/icons/backspace.svg')} aria-label={intl.formatMessage(messages.search)} className={classNames('svg-icon--backspace', { active: hasValue })} />
</div> </div>
</label> </label>
<Button onClick={handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button> <Button onClick={handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button>

View File

@ -491,8 +491,8 @@ class Audio extends React.PureComponent {
<div className='video-player__controls active'> <div className='video-player__controls active'>
<div className='video-player__buttons-bar'> <div className='video-player__buttons-bar'>
<div className='video-player__buttons left'> <div className='video-player__buttons left'>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon src={paused ? require('@tabler/icons/icons/player-play.svg') : require('@tabler/icons/icons/player-pause.svg')} /></button> <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon src={paused ? require('@tabler/icons/player-play.svg') : require('@tabler/icons/player-pause.svg')} /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon src={muted ? require('@tabler/icons/icons/volume-3.svg') : require('@tabler/icons/icons/volume.svg')} /></button> <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon src={muted ? require('@tabler/icons/volume-3.svg') : require('@tabler/icons/volume.svg')} /></button>
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}> <div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
<div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} /> <div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} />
@ -522,7 +522,7 @@ class Audio extends React.PureComponent {
download download
target='_blank' target='_blank'
> >
<Icon src={require('@tabler/icons/icons/download.svg')} /> <Icon src={require('@tabler/icons/download.svg')} />
</a> </a>
</div> </div>
</div> </div>

View File

@ -54,7 +54,7 @@ const AuthLayout = () => {
<div className='relative z-10 ml-auto flex items-center'> <div className='relative z-10 ml-auto flex items-center'>
<Button <Button
theme='link' theme='link'
icon={require('@tabler/icons/icons/user.svg')} icon={require('@tabler/icons/user.svg')}
to='/signup' to='/signup'
> >
{intl.formatMessage(messages.register)} {intl.formatMessage(messages.register)}

View File

@ -6,7 +6,7 @@ import CaptchaField, { NativeCaptchaField } from '../captcha';
describe('<CaptchaField />', () => { describe('<CaptchaField />', () => {
it('renders null by default', () => { it('renders null by default', () => {
render(<CaptchaField />); render(<CaptchaField idempotencyKey='' value='' />);
expect(screen.queryAllByRole('textbox')).toHaveLength(0); expect(screen.queryAllByRole('textbox')).toHaveLength(0);
}); });
@ -24,7 +24,9 @@ describe('<NativeCaptchaField />', () => {
render( render(
<NativeCaptchaField <NativeCaptchaField
captcha={captcha} captcha={captcha}
onChange={() => {}} // eslint-disable-line react/jsx-no-bind onChange={() => {}}
onClick={() => {}}
value=''
/>, />,
); );

View File

@ -13,7 +13,7 @@ describe('<LoginForm />', () => {
}), }),
}; };
render(<LoginForm handleSubmit={mockFn} isLoading={false} />, null, store); render(<LoginForm handleSubmit={mockFn} isLoading={false} />, undefined, store);
expect(screen.getByRole('heading')).toHaveTextContent(/sign in/i); expect(screen.getByRole('heading')).toHaveTextContent(/sign in/i);
}); });
@ -26,7 +26,7 @@ describe('<LoginForm />', () => {
}), }),
}; };
render(<LoginForm handleSubmit={mockFn} isLoading={false} />, null, store); render(<LoginForm handleSubmit={mockFn} isLoading={false} />, undefined, store);
expect(screen.getByRole('heading')).toHaveTextContent(/sign in/i); expect(screen.getByRole('heading')).toHaveTextContent(/sign in/i);
}); });

View File

@ -12,7 +12,7 @@ describe('<LoginPage />', () => {
}), }),
}; };
render(<LoginPage />, null, store); render(<LoginPage />, undefined, store);
expect(screen.getByRole('heading')).toHaveTextContent('Sign In'); expect(screen.getByRole('heading')).toHaveTextContent('Sign In');
}); });

View File

@ -130,7 +130,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
</>); </>);
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/check.svg'), icon: require('@tabler/icons/check.svg'),
heading: needsConfirmation heading: needsConfirmation
? intl.formatMessage(messages.needsConfirmationHeader) ? intl.formatMessage(messages.needsConfirmationHeader)
: needsApproval : needsApproval

View File

@ -33,7 +33,7 @@ const Backups = () => {
return [{ return [{
text: intl.formatMessage(messages.create), text: intl.formatMessage(messages.create),
action: handleCreateBackup, action: handleCreateBackup,
icon: require('@tabler/icons/icons/plus.svg'), icon: require('@tabler/icons/plus.svg'),
}]; }];
}; };

View File

@ -51,7 +51,7 @@ const Account: React.FC<IAccount> = ({ accountId }) => {
date: formattedBirthday, date: formattedBirthday,
})} })}
> >
<Icon src={require('@tabler/icons/icons/ballon.svg')} /> <Icon src={require('@tabler/icons/ballon.svg')} />
{formattedBirthday} {formattedBirthday}
</div> </div>
</div> </div>

View File

@ -144,7 +144,7 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
</div> </div>
<div className='chat-box__remove-attachment'> <div className='chat-box__remove-attachment'>
<IconButton <IconButton
src={require('@tabler/icons/icons/x.svg')} src={require('@tabler/icons/x.svg')}
onClick={handleRemoveFile} onClick={handleRemoveFile}
/> />
</div> </div>
@ -155,7 +155,7 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
const renderActionButton = () => { const renderActionButton = () => {
return canSubmit() ? ( return canSubmit() ? (
<IconButton <IconButton
src={require('@tabler/icons/icons/send.svg')} src={require('@tabler/icons/send.svg')}
title={intl.formatMessage(messages.send)} title={intl.formatMessage(messages.send)}
onClick={sendMessage} onClick={sendMessage}
/> />

View File

@ -219,7 +219,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
{ {
text: intl.formatMessage(messages.delete), text: intl.formatMessage(messages.delete),
action: handleDeleteMessage(chatMessage.chat_id, chatMessage.id), action: handleDeleteMessage(chatMessage.chat_id, chatMessage.id),
icon: require('@tabler/icons/icons/trash.svg'), icon: require('@tabler/icons/trash.svg'),
destructive: true, destructive: true,
}, },
]; ];
@ -228,7 +228,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
menu.push({ menu.push({
text: intl.formatMessage(messages.report), text: intl.formatMessage(messages.report),
action: handleReportUser(chatMessage.account_id), action: handleReportUser(chatMessage.account_id),
icon: require('@tabler/icons/icons/flag.svg'), icon: require('@tabler/icons/flag.svg'),
}); });
} }
@ -251,7 +251,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
<div className='chat-message__menu'> <div className='chat-message__menu'>
<DropdownMenuContainer <DropdownMenuContainer
items={menu} items={menu}
src={require('@tabler/icons/icons/dots.svg')} src={require('@tabler/icons/dots.svg')}
title={intl.formatMessage(messages.more)} title={intl.formatMessage(messages.more)}
/> />
</div> </div>

View File

@ -98,7 +98,7 @@ const ChatWindow: React.FC<IChatWindow> = ({ idx, chatId, windowState }) => {
@{getAcct(account, displayFqn)} @{getAcct(account, displayFqn)}
</button> </button>
<div className='pane__close'> <div className='pane__close'>
<IconButton src={require('@tabler/icons/icons/x.svg')} title='Close chat' onClick={handleChatClose(chat.id)} /> <IconButton src={require('@tabler/icons/x.svg')} title='Close chat' onClick={handleChatClose(chat.id)} />
</div> </div>
</HStack> </HStack>
<div className='pane__content'> <div className='pane__content'>

View File

@ -44,7 +44,7 @@ const Chat: React.FC<IChat> = ({ chatId, onClick }) => {
{attachment && ( {attachment && (
<Icon <Icon
className='chat__attachment-icon' className='chat__attachment-icon'
src={image ? require('@tabler/icons/icons/photo.svg') : require('@tabler/icons/icons/paperclip.svg')} src={image ? require('@tabler/icons/photo.svg') : require('@tabler/icons/paperclip.svg')}
/> />
)} )}
{content ? ( {content ? (

View File

@ -31,7 +31,7 @@ class ColumnSettings extends React.PureComponent {
<FormattedMessage id='community.column_settings.title' defaultMessage='Local timeline settings' /> <FormattedMessage id='community.column_settings.title' defaultMessage='Local timeline settings' />
</h1> </h1>
<div className='column-settings__close'> <div className='column-settings__close'>
<IconButton title={intl.formatMessage(messages.close)} src={require('@tabler/icons/icons/x.svg')} onClick={onClose} /> <IconButton title={intl.formatMessage(messages.close)} src={require('@tabler/icons/x.svg')} onClick={onClose} />
</div> </div>
</div> </div>

View File

@ -270,14 +270,14 @@ class ComposeForm extends ImmutablePureComponent {
} else if (this.props.privacy === 'direct') { } else if (this.props.privacy === 'direct') {
publishText = ( publishText = (
<> <>
<Icon src={require('@tabler/icons/icons/mail.svg')} /> <Icon src={require('@tabler/icons/mail.svg')} />
{intl.formatMessage(messages.message)} {intl.formatMessage(messages.message)}
</> </>
); );
} else if (this.props.privacy === 'private') { } else if (this.props.privacy === 'private') {
publishText = ( publishText = (
<> <>
<Icon src={require('@tabler/icons/icons/lock.svg')} /> <Icon src={require('@tabler/icons/lock.svg')} />
{intl.formatMessage(messages.publish)} {intl.formatMessage(messages.publish)}
</> </>
); );

View File

@ -365,7 +365,7 @@ class EmojiPickerDropdown extends React.PureComponent {
'pulse-loading': active && loading, 'pulse-loading': active && loading,
})} })}
alt='😀' alt='😀'
src={require('@tabler/icons/icons/mood-happy.svg')} src={require('@tabler/icons/mood-happy.svg')}
title={title} title={title}
aria-label={title} aria-label={title}
aria-expanded={active} aria-expanded={active}

View File

@ -18,7 +18,7 @@ const MarkdownButton: React.FC<IMarkdownButton> = ({ active, onClick }) => {
return ( return (
<ComposeFormButton <ComposeFormButton
icon={require('@tabler/icons/icons/markdown.svg')} icon={require('@tabler/icons/markdown.svg')}
title={intl.formatMessage(active ? messages.marked : messages.unmarked)} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}
active={active} active={active}
onClick={onClick} onClick={onClick}

View File

@ -24,7 +24,7 @@ const PollButton: React.FC<IPollButton> = ({ active, unavailable, disabled, onCl
return ( return (
<ComposeFormButton <ComposeFormButton
icon={require('@tabler/icons/icons/chart-bar.svg')} icon={require('@tabler/icons/chart-bar.svg')}
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)} title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
active={active} active={active}
disabled={disabled} disabled={disabled}

View File

@ -161,10 +161,10 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
const [placement, setPlacement] = useState('bottom'); const [placement, setPlacement] = useState('bottom');
const options = [ const options = [
{ icon: require('@tabler/icons/icons/world.svg'), value: 'public', text: intl.formatMessage(messages.public_short), meta: intl.formatMessage(messages.public_long) }, { icon: require('@tabler/icons/world.svg'), value: 'public', text: intl.formatMessage(messages.public_short), meta: intl.formatMessage(messages.public_long) },
{ icon: require('@tabler/icons/icons/lock-open.svg'), value: 'unlisted', text: intl.formatMessage(messages.unlisted_short), meta: intl.formatMessage(messages.unlisted_long) }, { icon: require('@tabler/icons/lock-open.svg'), value: 'unlisted', text: intl.formatMessage(messages.unlisted_short), meta: intl.formatMessage(messages.unlisted_long) },
{ icon: require('@tabler/icons/icons/lock.svg'), value: 'private', text: intl.formatMessage(messages.private_short), meta: intl.formatMessage(messages.private_long) }, { icon: require('@tabler/icons/lock.svg'), value: 'private', text: intl.formatMessage(messages.private_short), meta: intl.formatMessage(messages.private_long) },
{ icon: require('@tabler/icons/icons/mail.svg'), value: 'direct', text: intl.formatMessage(messages.direct_short), meta: intl.formatMessage(messages.direct_long) }, { icon: require('@tabler/icons/mail.svg'), value: 'direct', text: intl.formatMessage(messages.direct_short), meta: intl.formatMessage(messages.direct_long) },
]; ];
const handleToggle: React.MouseEventHandler<HTMLButtonElement> = (e) => { const handleToggle: React.MouseEventHandler<HTMLButtonElement> = (e) => {

View File

@ -26,7 +26,7 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ status, hideActions, onCanc
if (!hideActions && onCancel) { if (!hideActions && onCancel) {
actions = { actions = {
onActionClick: handleClick, onActionClick: handleClick,
actionIcon: require('@tabler/icons/icons/x.svg'), actionIcon: require('@tabler/icons/x.svg'),
actionAlignment: 'top', actionAlignment: 'top',
actionTitle: 'Dismiss', actionTitle: 'Dismiss',
}; };
@ -39,6 +39,7 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ status, hideActions, onCanc
id={status.getIn(['account', 'id']) as string} id={status.getIn(['account', 'id']) as string}
timestamp={status.created_at} timestamp={status.created_at}
showProfileHoverCard={false} showProfileHoverCard={false}
withLinkToProfile={false}
/> />
<Text <Text

View File

@ -28,7 +28,7 @@ const ScheduleButton: React.FC<IScheduleButton> = ({ active, unavailable, disabl
return ( return (
<ComposeFormButton <ComposeFormButton
icon={require('@tabler/icons/icons/calendar-stats.svg')} icon={require('@tabler/icons/calendar-stats.svg')}
title={intl.formatMessage(active ? messages.remove_schedule : messages.add_schedule)} title={intl.formatMessage(active ? messages.remove_schedule : messages.add_schedule)}
active={active} active={active}
disabled={disabled} disabled={disabled}

View File

@ -72,7 +72,7 @@ const ScheduleForm: React.FC = () => {
<IconButton <IconButton
iconClassName='w-4 h-4' iconClassName='w-4 h-4'
className='bg-transparent text-gray-400 hover:text-gray-600' className='bg-transparent text-gray-400 hover:text-gray-600'
src={require('@tabler/icons/icons/x.svg')} src={require('@tabler/icons/x.svg')}
onClick={handleRemove} onClick={handleRemove}
title={intl.formatMessage(messages.remove)} title={intl.formatMessage(messages.remove)}
/> />

View File

@ -105,7 +105,7 @@ const Search = (props: ISearch) => {
const makeMenu = () => [ const makeMenu = () => [
{ {
text: intl.formatMessage(messages.action, { query: value }), text: intl.formatMessage(messages.action, { query: value }),
icon: require('@tabler/icons/icons/search.svg'), icon: require('@tabler/icons/search.svg'),
action: handleSubmit, action: handleSubmit,
}, },
]; ];
@ -140,12 +140,12 @@ const Search = (props: ISearch) => {
onClick={handleClear} onClick={handleClear}
> >
<SvgIcon <SvgIcon
src={require('@tabler/icons/icons/search.svg')} src={require('@tabler/icons/search.svg')}
className={classNames('h-4 w-4 text-gray-400', { hidden: hasValue })} className={classNames('h-4 w-4 text-gray-400', { hidden: hasValue })}
/> />
<SvgIcon <SvgIcon
src={require('@tabler/icons/icons/x.svg')} src={require('@tabler/icons/x.svg')}
className={classNames('h-4 w-4 text-gray-400', { hidden: !hasValue })} className={classNames('h-4 w-4 text-gray-400', { hidden: !hasValue })}
aria-label={intl.formatMessage(messages.placeholder)} aria-label={intl.formatMessage(messages.placeholder)}
/> />

View File

@ -18,7 +18,7 @@ const SpoilerButton: React.FC<ISpoilerButton> = ({ active, onClick }) => {
return ( return (
<ComposeFormButton <ComposeFormButton
icon={require('@tabler/icons/icons/alert-triangle.svg')} icon={require('@tabler/icons/alert-triangle.svg')}
title={intl.formatMessage(active ? messages.marked : messages.unmarked)} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}
active={active} active={active}
onClick={onClick} onClick={onClick}

View File

@ -12,13 +12,13 @@ import Motion from '../../ui/util/optional_motion';
import type { Map as ImmutableMap } from 'immutable'; import type { Map as ImmutableMap } from 'immutable';
const bookIcon = require('@tabler/icons/icons/book.svg'); const bookIcon = require('@tabler/icons/book.svg');
const fileAnalyticsIcon = require('@tabler/icons/icons/file-analytics.svg'); const fileAnalyticsIcon = require('@tabler/icons/file-analytics.svg');
const fileCodeIcon = require('@tabler/icons/icons/file-code.svg'); const fileCodeIcon = require('@tabler/icons/file-code.svg');
const fileTextIcon = require('@tabler/icons/icons/file-text.svg'); const fileTextIcon = require('@tabler/icons/file-text.svg');
const fileZipIcon = require('@tabler/icons/icons/file-zip.svg'); const fileZipIcon = require('@tabler/icons/file-zip.svg');
const defaultIcon = require('@tabler/icons/icons/paperclip.svg'); const defaultIcon = require('@tabler/icons/paperclip.svg');
const presentationIcon = require('@tabler/icons/icons/presentation.svg'); const presentationIcon = require('@tabler/icons/presentation.svg');
export const MIMETYPE_ICONS: Record<string, string> = { export const MIMETYPE_ICONS: Record<string, string> = {
'application/x-freearc': fileZipIcon, 'application/x-freearc': fileZipIcon,
@ -157,7 +157,7 @@ const Upload: React.FC<IUpload> = (props) => {
<div className={classNames('compose-form__upload__actions', { active })}> <div className={classNames('compose-form__upload__actions', { active })}>
<IconButton <IconButton
onClick={handleUndoClick} onClick={handleUndoClick}
src={require('@tabler/icons/icons/x.svg')} src={require('@tabler/icons/x.svg')}
text={<FormattedMessage id='upload_form.undo' defaultMessage='Delete' />} text={<FormattedMessage id='upload_form.undo' defaultMessage='Delete' />}
/> />
@ -165,7 +165,7 @@ const Upload: React.FC<IUpload> = (props) => {
{(mediaType !== 'unknown' && Boolean(props.media.get('url'))) && ( {(mediaType !== 'unknown' && Boolean(props.media.get('url'))) && (
<IconButton <IconButton
onClick={handleOpenModal} onClick={handleOpenModal}
src={require('@tabler/icons/icons/zoom-in.svg')} src={require('@tabler/icons/zoom-in.svg')}
text={<FormattedMessage id='upload_form.preview' defaultMessage='Preview' />} text={<FormattedMessage id='upload_form.preview' defaultMessage='Preview' />}
/> />
)} )}

View File

@ -48,8 +48,8 @@ const UploadButton: React.FC<IUploadButton> = ({
} }
const src = onlyImages(attachmentTypes) const src = onlyImages(attachmentTypes)
? require('@tabler/icons/icons/photo.svg') ? require('@tabler/icons/photo.svg')
: require('@tabler/icons/icons/paperclip.svg'); : require('@tabler/icons/paperclip.svg');
return ( return (
<div> <div>

View File

@ -42,12 +42,12 @@ const CryptoAddress: React.FC<ICryptoAddress> = (props): JSX.Element => {
<HStack alignItems='center' className='ml-auto'> <HStack alignItems='center' className='ml-auto'>
<a className='text-gray-500 ml-1' href='#' onClick={handleModalClick}> <a className='text-gray-500 ml-1' href='#' onClick={handleModalClick}>
<Icon src={require('@tabler/icons/icons/qrcode.svg')} size={20} /> <Icon src={require('@tabler/icons/qrcode.svg')} size={20} />
</a> </a>
{explorerUrl && ( {explorerUrl && (
<a className='text-gray-500 ml-1' href={explorerUrl} target='_blank'> <a className='text-gray-500 ml-1' href={explorerUrl} target='_blank'>
<Icon src={require('@tabler/icons/icons/external-link.svg')} size={20} /> <Icon src={require('@tabler/icons/external-link.svg')} size={20} />
</a> </a>
)} )}
</HStack> </HStack>

View File

@ -30,7 +30,7 @@ const DetailedCryptoAddress: React.FC<IDetailedCryptoAddress> = ({ address, tick
<div className='crypto-address__title'>{title || ticker.toUpperCase()}</div> <div className='crypto-address__title'>{title || ticker.toUpperCase()}</div>
<div className='crypto-address__actions'> <div className='crypto-address__actions'>
{explorerUrl && <a href={explorerUrl} target='_blank'> {explorerUrl && <a href={explorerUrl} target='_blank'>
<Icon src={require('@tabler/icons/icons/external-link.svg')} /> <Icon src={require('@tabler/icons/external-link.svg')} />
</a>} </a>}
</div> </div>
</div> </div>

View File

@ -32,7 +32,7 @@ const Developers = () => {
<Column label={intl.formatMessage(messages.heading)}> <Column label={intl.formatMessage(messages.heading)}>
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2'> <div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2'>
<Link to='/developers/apps/create' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'> <Link to='/developers/apps/create' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'>
<SvgIcon src={require('@tabler/icons/icons/apps.svg')} className='dark:text-gray-100' /> <SvgIcon src={require('@tabler/icons/apps.svg')} className='dark:text-gray-100' />
<Text> <Text>
<FormattedMessage id='developers.navigation.app_create_label' defaultMessage='Create an app' /> <FormattedMessage id='developers.navigation.app_create_label' defaultMessage='Create an app' />
@ -40,7 +40,7 @@ const Developers = () => {
</Link> </Link>
<Link to='/developers/settings_store' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'> <Link to='/developers/settings_store' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'>
<SvgIcon src={require('@tabler/icons/icons/code-plus.svg')} className='dark:text-gray-100' /> <SvgIcon src={require('@tabler/icons/code-plus.svg')} className='dark:text-gray-100' />
<Text> <Text>
<FormattedMessage id='developers.navigation.settings_store_label' defaultMessage='Settings store' /> <FormattedMessage id='developers.navigation.settings_store_label' defaultMessage='Settings store' />
@ -48,7 +48,7 @@ const Developers = () => {
</Link> </Link>
<Link to='/developers/timeline' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'> <Link to='/developers/timeline' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'>
<SvgIcon src={require('@tabler/icons/icons/home.svg')} className='dark:text-gray-100' /> <SvgIcon src={require('@tabler/icons/home.svg')} className='dark:text-gray-100' />
<Text> <Text>
<FormattedMessage id='developers.navigation.test_timeline_label' defaultMessage='Test timeline' /> <FormattedMessage id='developers.navigation.test_timeline_label' defaultMessage='Test timeline' />
@ -56,7 +56,7 @@ const Developers = () => {
</Link> </Link>
<Link to='/error' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'> <Link to='/error' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'>
<SvgIcon src={require('@tabler/icons/icons/mood-sad.svg')} className='dark:text-gray-100' /> <SvgIcon src={require('@tabler/icons/mood-sad.svg')} className='dark:text-gray-100' />
<Text> <Text>
<FormattedMessage id='developers.navigation.intentional_error_label' defaultMessage='Trigger an error' /> <FormattedMessage id='developers.navigation.intentional_error_label' defaultMessage='Trigger an error' />
@ -64,7 +64,7 @@ const Developers = () => {
</Link> </Link>
<Link to='/error/network' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'> <Link to='/error/network' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'>
<SvgIcon src={require('@tabler/icons/icons/refresh.svg')} className='dark:text-gray-100' /> <SvgIcon src={require('@tabler/icons/refresh.svg')} className='dark:text-gray-100' />
<Text> <Text>
<FormattedMessage id='developers.navigation.network_error_label' defaultMessage='Network error' /> <FormattedMessage id='developers.navigation.network_error_label' defaultMessage='Network error' />
@ -72,7 +72,7 @@ const Developers = () => {
</Link> </Link>
<button onClick={leaveDevelopers} className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'> <button onClick={leaveDevelopers} className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'>
<SvgIcon src={require('@tabler/icons/icons/logout.svg')} className='dark:text-gray-100' /> <SvgIcon src={require('@tabler/icons/logout.svg')} className='dark:text-gray-100' />
<Text> <Text>
<FormattedMessage id='developers.navigation.leave_developers_label' defaultMessage='Leave developers' /> <FormattedMessage id='developers.navigation.leave_developers_label' defaultMessage='Leave developers' />

View File

@ -4,9 +4,19 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { updateNotificationSettings } from 'soapbox/actions/accounts'; import { updateNotificationSettings } from 'soapbox/actions/accounts';
import { patchMe } from 'soapbox/actions/me'; import { patchMe } from 'soapbox/actions/me';
import snackbar from 'soapbox/actions/snackbar'; import snackbar from 'soapbox/actions/snackbar';
import BirthdayInput from 'soapbox/components/birthday_input';
import List, { ListItem } from 'soapbox/components/list'; import List, { ListItem } from 'soapbox/components/list';
import { Button, Column, Form, FormActions, FormGroup, Input, Textarea, HStack, Toggle, FileInput } from 'soapbox/components/ui'; import {
Button,
Column,
FileInput,
Form,
FormActions,
FormGroup,
HStack,
Input,
Textarea,
Toggle,
} from 'soapbox/components/ui';
import Streamfield, { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield'; import Streamfield, { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks'; import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks';
import { normalizeAccount } from 'soapbox/normalizers'; import { normalizeAccount } from 'soapbox/normalizers';
@ -25,25 +35,6 @@ const hidesNetwork = (account: Account): boolean => {
return Boolean(hide_followers && hide_follows && hide_followers_count && hide_follows_count); return Boolean(hide_followers && hide_follows && hide_followers_count && hide_follows_count);
}; };
/** Converts JSON objects to FormData. */
// https://stackoverflow.com/a/60286175/8811886
// @ts-ignore
const toFormData = (f => f(f))(h => f => f(x => h(h)(f)(x)))(f => fd => pk => d => {
if (d instanceof Object) {
// eslint-disable-next-line consistent-return
Object.keys(d).forEach(k => {
const v = d[k];
if (pk) k = `${pk}[${k}]`;
if (v instanceof Object && !(v instanceof Date) && !(v instanceof File)) {
return f(fd)(k)(v);
} else {
fd.append(k, v);
}
});
}
return fd;
})(new FormData())();
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.edit_profile', defaultMessage: 'Edit profile' }, heading: { id: 'column.edit_profile', defaultMessage: 'Edit profile' },
header: { id: 'edit_profile.header', defaultMessage: 'Edit Profile' }, header: { id: 'edit_profile.header', defaultMessage: 'Edit Profile' },
@ -205,9 +196,8 @@ const EditProfile: React.FC = () => {
const handleSubmit: React.FormEventHandler = (event) => { const handleSubmit: React.FormEventHandler = (event) => {
const promises = []; const promises = [];
const formData = toFormData(data);
promises.push(dispatch(patchMe(formData))); promises.push(dispatch(patchMe(data, true)));
if (features.muteStrangers) { if (features.muteStrangers) {
promises.push( promises.push(
@ -242,10 +232,6 @@ const EditProfile: React.FC = () => {
}; };
}; };
const handleBirthdayChange = (date: string) => {
updateData('birthday', date);
};
const handleHideNetworkChange: React.ChangeEventHandler<HTMLInputElement> = e => { const handleHideNetworkChange: React.ChangeEventHandler<HTMLInputElement> = e => {
const hide = e.target.checked; const hide = e.target.checked;
@ -329,9 +315,12 @@ const EditProfile: React.FC = () => {
<FormGroup <FormGroup
labelText={<FormattedMessage id='edit_profile.fields.birthday_label' defaultMessage='Birthday' />} labelText={<FormattedMessage id='edit_profile.fields.birthday_label' defaultMessage='Birthday' />}
> >
<BirthdayInput <Input
type='text'
value={data.birthday} value={data.birthday}
onChange={handleBirthdayChange} onChange={handleTextChange('birthday')}
placeholder='YYYY-MM-DD'
pattern='\d{4}-\d{2}-\d{2}'
/> />
</FormGroup> </FormGroup>
)} )}

View File

@ -1,9 +1,10 @@
// @ts-ignore
import { emojiIndex } from 'emoji-mart'; import { emojiIndex } from 'emoji-mart';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import { search } from '../emoji_mart_search_light'; import { search } from '../emoji_mart_search_light';
const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']); const trimEmojis = (emoji: any) => pick(emoji, ['id', 'unified', 'native', 'custom']);
describe('emoji_index', () => { describe('emoji_index', () => {
it('should give same result for emoji_index_light and emoji-mart', () => { it('should give same result for emoji_index_light and emoji-mart', () => {
@ -46,7 +47,7 @@ describe('emoji_index', () => {
}); });
it('can include/exclude categories', () => { it('can include/exclude categories', () => {
expect(search('flag', { include: ['people'] })).toEqual([]); expect(search('flag', { include: ['people'] } as any)).toEqual([]);
expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([]); expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([]);
}); });
@ -63,9 +64,8 @@ describe('emoji_index', () => {
custom: true, custom: true,
}, },
]; ];
search('', { custom }); search('', { custom } as any);
emojiIndex.search('', { custom }); emojiIndex.search('', { custom });
const expected = [];
const lightExpected = [ const lightExpected = [
{ {
id: 'mastodon', id: 'mastodon',
@ -73,7 +73,7 @@ describe('emoji_index', () => {
}, },
]; ];
expect(search('masto').map(trimEmojis)).toEqual(lightExpected); expect(search('masto').map(trimEmojis)).toEqual(lightExpected);
expect(emojiIndex.search('masto').map(trimEmojis)).toEqual(expected); expect(emojiIndex.search('masto').map(trimEmojis)).toEqual([]);
}); });
it('(different behavior from emoji-mart) erases custom emoji if another is passed', () => { it('(different behavior from emoji-mart) erases custom emoji if another is passed', () => {
@ -89,11 +89,10 @@ describe('emoji_index', () => {
custom: true, custom: true,
}, },
]; ];
search('', { custom }); search('', { custom } as any);
emojiIndex.search('', { custom }); emojiIndex.search('', { custom });
const expected = []; expect(search('masto', { custom: [] } as any).map(trimEmojis)).toEqual([]);
expect(search('masto', { custom: [] }).map(trimEmojis)).toEqual(expected); expect(emojiIndex.search('masto').map(trimEmojis)).toEqual([]);
expect(emojiIndex.search('masto').map(trimEmojis)).toEqual(expected);
}); });
it('handles custom emoji', () => { it('handles custom emoji', () => {
@ -109,7 +108,7 @@ describe('emoji_index', () => {
custom: true, custom: true,
}, },
]; ];
search('', { custom }); search('', { custom } as any);
emojiIndex.search('', { custom }); emojiIndex.search('', { custom });
const expected = [ const expected = [
{ {
@ -117,15 +116,15 @@ describe('emoji_index', () => {
custom: true, custom: true,
}, },
]; ];
expect(search('masto', { custom }).map(trimEmojis)).toEqual(expected); expect(search('masto', { custom } as any).map(trimEmojis)).toEqual(expected);
expect(emojiIndex.search('masto', { custom }).map(trimEmojis)).toEqual(expected); expect(emojiIndex.search('masto', { custom }).map(trimEmojis)).toEqual(expected);
}); });
it('should filter only emojis we care about, exclude pineapple', () => { it('should filter only emojis we care about, exclude pineapple', () => {
const emojisToShowFilter = emoji => emoji.unified !== '1F34D'; const emojisToShowFilter = (emoji: any) => emoji.unified !== '1F34D';
expect(search('apple', { emojisToShowFilter }).map((obj) => obj.id)) expect(search('apple', { emojisToShowFilter } as any).map((obj: any) => obj.id))
.not.toContain('pineapple'); .not.toContain('pineapple');
expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id)) expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj: any) => obj.id))
.not.toContain('pineapple'); .not.toContain('pineapple');
}); });

View File

@ -50,8 +50,8 @@ class InstanceRestrictions extends ImmutablePureComponent {
if (followers_only) { if (followers_only) {
items.push(( items.push((
<Text key='followers_only'> <Text key='followers_only' className='flex items-center gap-2' theme='muted'>
<Icon className='mr-2' src={require('@tabler/icons/icons/lock.svg')} /> <Icon src={require('@tabler/icons/lock.svg')} />
<FormattedMessage <FormattedMessage
id='federation_restriction.followers_only' id='federation_restriction.followers_only'
defaultMessage='Hidden except to followers' defaultMessage='Hidden except to followers'
@ -60,8 +60,8 @@ class InstanceRestrictions extends ImmutablePureComponent {
)); ));
} else if (federated_timeline_removal) { } else if (federated_timeline_removal) {
items.push(( items.push((
<Text key='federated_timeline_removal'> <Text key='federated_timeline_removal' className='flex items-center gap-2' theme='muted'>
<Icon className='mr-2' src={require('@tabler/icons/icons/lock-open.svg')} /> <Icon src={require('@tabler/icons/lock-open.svg')} />
<FormattedMessage <FormattedMessage
id='federation_restriction.federated_timeline_removal' id='federation_restriction.federated_timeline_removal'
defaultMessage='Fediverse timeline removal' defaultMessage='Fediverse timeline removal'
@ -72,8 +72,8 @@ class InstanceRestrictions extends ImmutablePureComponent {
if (fullMediaRemoval) { if (fullMediaRemoval) {
items.push(( items.push((
<Text key='full_media_removal'> <Text key='full_media_removal' className='flex items-center gap-2' theme='muted'>
<Icon className='mr-2' src={require('@tabler/icons/icons/photo-off.svg')} /> <Icon src={require('@tabler/icons/photo-off.svg')} />
<FormattedMessage <FormattedMessage
id='federation_restriction.full_media_removal' id='federation_restriction.full_media_removal'
defaultMessage='Full media removal' defaultMessage='Full media removal'
@ -82,8 +82,8 @@ class InstanceRestrictions extends ImmutablePureComponent {
)); ));
} else if (partialMediaRemoval) { } else if (partialMediaRemoval) {
items.push(( items.push((
<Text key='partial_media_removal'> <Text key='partial_media_removal' className='flex items-center gap-2' theme='muted'>
<Icon className='mr-2' src={require('@tabler/icons/icons/photo-off.svg')} /> <Icon src={require('@tabler/icons/photo-off.svg')} />
<FormattedMessage <FormattedMessage
id='federation_restriction.partial_media_removal' id='federation_restriction.partial_media_removal'
defaultMessage='Partial media removal' defaultMessage='Partial media removal'
@ -94,8 +94,8 @@ class InstanceRestrictions extends ImmutablePureComponent {
if (!fullMediaRemoval && media_nsfw) { if (!fullMediaRemoval && media_nsfw) {
items.push(( items.push((
<Text key='media_nsfw'> <Text key='media_nsfw' className='flex items-center gap-2' theme='muted'>
<Icon className='mr-2' id='eye-slash' /> <Icon id='eye-slash' />
<FormattedMessage <FormattedMessage
id='federation_restriction.media_nsfw' id='federation_restriction.media_nsfw'
defaultMessage='Attachments marked NSFW' defaultMessage='Attachments marked NSFW'
@ -116,8 +116,8 @@ class InstanceRestrictions extends ImmutablePureComponent {
if (remoteInstance.getIn(['federation', 'reject']) === true) { if (remoteInstance.getIn(['federation', 'reject']) === true) {
return ( return (
<Text> <Text className='flex items-center gap-2' theme='muted'>
<Icon className='mr-2' id='times' /> <Icon id='times' />
<FormattedMessage <FormattedMessage
id='remote_instance.federation_panel.restricted_message' id='remote_instance.federation_panel.restricted_message'
defaultMessage='{siteTitle} blocks all activities from {host}.' defaultMessage='{siteTitle} blocks all activities from {host}.'
@ -128,7 +128,7 @@ class InstanceRestrictions extends ImmutablePureComponent {
} else if (hasRestrictions(remoteInstance)) { } else if (hasRestrictions(remoteInstance)) {
return [ return [
( (
<Text> <Text theme='muted'>
<FormattedMessage <FormattedMessage
id='remote_instance.federation_panel.some_restrictions_message' id='remote_instance.federation_panel.some_restrictions_message'
defaultMessage='{siteTitle} has placed some restrictions on {host}.' defaultMessage='{siteTitle} has placed some restrictions on {host}.'
@ -140,8 +140,8 @@ class InstanceRestrictions extends ImmutablePureComponent {
]; ];
} else { } else {
return ( return (
<Text> <Text className='flex items-center gap-2' theme='muted'>
<Icon className='mr-2' id='check' /> <Icon id='check' />
<FormattedMessage <FormattedMessage
id='remote_instance.federation_panel.no_restrictions_message' id='remote_instance.federation_panel.no_restrictions_message'
defaultMessage='{siteTitle} has placed no restrictions on {host}.' defaultMessage='{siteTitle} has placed no restrictions on {host}.'
@ -153,7 +153,11 @@ class InstanceRestrictions extends ImmutablePureComponent {
} }
render() { render() {
return <div className='instance-restrictions'>{this.renderContent()}</div>; return (
<div className='py-1 pl-4 mb-4 border-solid border-l-[3px] border-gray-300 dark:border-gray-500'>
{this.renderContent()}
</div>
);
} }
} }

Some files were not shown because too many files have changed in this diff Show More